[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import { FileCode, Search, Zap } from 'lucide-react';
import React, { memo, useMemo, useState } from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import { cn } from '../../lib/utils';
import type { Snippet } from '../../types';
import { FixedSizeVirtualList } from '../ui/FixedSizeVirtualList';
import { Input } from '../ui/input';
import { SnippetCommandTooltipContent } from './SnippetCommandTooltipContent';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
const ROW_HEIGHT = 34;
export interface SnippetCommandPickerProps {
snippets: Snippet[];
selectedId?: string | null;
onSelect: (snippet: Snippet) => void;
className?: string;
showTitle?: boolean;
}
export const SnippetCommandPicker = memo(function SnippetCommandPicker({
snippets,
selectedId,
onSelect,
className,
showTitle = true,
}: SnippetCommandPickerProps) {
const { t } = useI18n();
const [search, setSearch] = useState('');
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
const sorted = [...snippets].sort((a, b) => a.label.localeCompare(b.label));
if (!q) return sorted;
return sorted.filter(
(snippet) =>
snippet.label.toLowerCase().includes(q)
|| snippet.command.toLowerCase().includes(q)
|| (snippet.package || '').toLowerCase().includes(q),
);
}, [search, snippets]);
const listItems = useMemo(
() => filtered.map((snippet) => ({ key: snippet.id, snippet })),
[filtered],
);
return (
<TooltipProvider delayDuration={300}>
<div
className={cn(
'flex min-h-0 flex-col overflow-hidden rounded-md border border-border/60 bg-muted/20',
className,
)}
>
<div className="shrink-0 border-b border-border/50 px-2 py-1.5">
{showTitle && (
<p className="mb-1.5 px-0.5 text-[10px] font-medium text-muted-foreground">
{t('systemManager.tmux.pickSnippet')}
</p>
)}
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('snippets.searchPlaceholder')}
className="h-7 border-none bg-background/60 pl-7 text-xs"
disabled={snippets.length === 0}
/>
</div>
</div>
<div className="min-h-0 flex-1">
{snippets.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center px-4 py-6 text-center text-muted-foreground">
<Zap size={22} className="mb-2 opacity-40" />
<p className="text-xs">{t('systemManager.tmux.pickSnippetEmpty')}</p>
</div>
) : filtered.length === 0 ? (
<div className="px-3 py-4 text-center text-xs italic text-muted-foreground">
{t('common.noResultsFound')}
</div>
) : (
<FixedSizeVirtualList
className="h-full"
contentClassName="py-1"
items={listItems}
itemHeight={ROW_HEIGHT}
getItemKey={(item) => item.key}
renderItem={(item) => {
const { snippet } = item;
const selected = selectedId === snippet.id;
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onSelect(snippet)}
className={cn(
'flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors',
selected
? 'bg-primary/10 text-foreground'
: 'hover:bg-accent/50',
)}
>
<FileCode size={12} className="shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate text-xs font-medium">{snippet.label}</span>
{snippet.package ? (
<span className="max-w-[42%] shrink-0 truncate text-[10px] text-muted-foreground">
{snippet.package}
</span>
) : null}
</button>
</TooltipTrigger>
<TooltipContent side="right" align="start">
<SnippetCommandTooltipContent label={snippet.label} command={snippet.command} />
</TooltipContent>
</Tooltip>
);
}}
/>
)}
</div>
</div>
</TooltipProvider>
);
});

View File

@@ -0,0 +1,29 @@
import React from "react";
import { formatSnippetCommandTooltip } from "@/domain/snippetPreview";
import { cn } from "@/lib/utils";
export function SnippetCommandTooltipContent({
label,
command,
className,
fallback,
}: {
label?: string;
command: string;
className?: string;
fallback?: string;
}) {
const preview = formatSnippetCommandTooltip(command);
return (
<div className={cn("max-w-sm", className)}>
{label ? (
<div className="mb-1 break-all text-xs font-medium">{label}</div>
) : null}
<pre className="max-h-36 overflow-hidden font-mono text-[11px] leading-snug whitespace-pre-wrap break-all">
{preview || fallback || "—"}
</pre>
</div>
);
}

View File

@@ -0,0 +1,210 @@
import { Maximize2 } from 'lucide-react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import { STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT } from '@/infrastructure/config/storageKeys.ts';
import { localStorageAdapter } from '@/infrastructure/persistence/localStorageAdapter.ts';
import { Button } from '../ui/button';
import {
ScriptCodeEditor,
type ScriptCodeEditorHandle,
} from '../scripts/ScriptCodeEditor';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
const DEFAULT_HEIGHT = 120;
const MIN_HEIGHT = 80;
const MAX_HEIGHT = 520;
function clampHeight(height: number, minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT): number {
return Math.max(minHeight, Math.min(maxHeight, height));
}
function readStoredHeight({
defaultHeight,
minHeight,
maxHeight,
persistHeight,
}: {
defaultHeight: number;
minHeight: number;
maxHeight: number;
persistHeight: boolean;
}): number {
if (!persistHeight) return clampHeight(defaultHeight, minHeight, maxHeight);
const stored = localStorageAdapter.readNumber(STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT);
if (stored === null) return clampHeight(defaultHeight, minHeight, maxHeight);
return clampHeight(stored, minHeight, maxHeight);
}
export interface SnippetScriptEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
id?: string;
/** Shown on the same row as the expand button (e.g. "Script *"). */
label?: string;
defaultHeight?: number;
minHeight?: number;
maxHeight?: number;
persistHeight?: boolean;
/** Save or submit the surrounding form with Cmd/Ctrl+Enter. */
onSubmitShortcut?: () => void;
}
export const SnippetScriptEditor: React.FC<SnippetScriptEditorProps> = ({
value,
onChange,
placeholder,
id,
label,
defaultHeight = DEFAULT_HEIGHT,
minHeight = MIN_HEIGHT,
maxHeight = MAX_HEIGHT,
persistHeight = true,
onSubmitShortcut,
}) => {
const { t } = useI18n();
const [height, setHeight] = useState(() => readStoredHeight({
defaultHeight,
minHeight,
maxHeight,
persistHeight,
}));
const [modalOpen, setModalOpen] = useState(false);
const dragRef = useRef<{ startY: number; startHeight: number } | null>(null);
const inlineEditorRef = useRef<ScriptCodeEditorHandle>(null);
const heightRef = useRef(height);
heightRef.current = height;
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
dragRef.current = { startY: e.clientY, startHeight: heightRef.current };
document.body.style.cursor = 'ns-resize';
document.body.style.userSelect = 'none';
}, []);
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (!dragRef.current) return;
const delta = e.clientY - dragRef.current.startY;
setHeight(clampHeight(dragRef.current.startHeight + delta, minHeight, maxHeight));
};
const onUp = () => {
if (dragRef.current && persistHeight) {
localStorageAdapter.writeNumber(
STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT,
heightRef.current,
);
}
dragRef.current = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [maxHeight, minHeight, persistHeight]);
return (
<>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-2 min-h-7">
{label ? (
id ? (
<label
id={`${id}-label`}
className="text-xs font-semibold text-muted-foreground shrink-0 cursor-text"
onClick={() => inlineEditorRef.current?.focus()}
>
{label}
</label>
) : (
<p className="text-xs font-semibold text-muted-foreground shrink-0">{label}</p>
)
) : (
<span className="flex-1" aria-hidden />
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => setModalOpen(true)}
aria-label={t('snippets.scriptEditor.expand')}
>
<Maximize2 size={14} />
{t('snippets.scriptEditor.expand')}
</Button>
</TooltipTrigger>
<TooltipContent>{t('snippets.scriptEditor.expand')}</TooltipContent>
</Tooltip>
</div>
<div
className="relative overflow-hidden rounded-md border border-border/60 bg-background"
style={{ height }}
>
<ScriptCodeEditor
ref={inlineEditorRef}
value={value}
onChange={onChange}
language="shell"
fill
height={height}
ariaLabel={label || placeholder}
placeholder={placeholder}
tabFocusMode
onSubmitShortcut={onSubmitShortcut}
/>
<div
role="separator"
aria-orientation="horizontal"
aria-label={t('snippets.scriptEditor.resize')}
className="absolute bottom-0 left-0 right-0 z-10 flex h-2.5 cursor-ns-resize items-center justify-center rounded-b-md hover:bg-muted/40"
onMouseDown={handleResizeStart}
>
<div className="h-0.5 w-10 rounded-full bg-border/80" />
</div>
</div>
</div>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="max-w-4xl w-[min(90vw,56rem)] h-[min(85vh,640px)] flex flex-col gap-0 p-0">
<DialogHeader className="px-6 pt-6 pb-3 shrink-0">
<DialogTitle>{t('snippets.scriptEditor.modalTitle')}</DialogTitle>
</DialogHeader>
<div className="flex-1 min-h-0 mx-6 mb-3 overflow-hidden rounded-md border border-border/60 bg-background">
<ScriptCodeEditor
value={value}
onChange={onChange}
language="shell"
fill
autoFocus
active={modalOpen}
ariaLabel={label || placeholder}
placeholder={placeholder}
onSubmitShortcut={onSubmitShortcut}
/>
</div>
<DialogFooter className="px-6 pb-6 pt-2 shrink-0">
<Button type="button" onClick={() => setModalOpen(false)}>
{t('common.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { SnippetTargetsSection } from './SnippetTargetsSection';
const t = (key: string) => ({
'snippets.targets.title': 'Targets',
'snippets.targets.selectHosts': 'Select hosts',
'snippets.targets.selectGroups': 'Select groups',
'snippets.targets.add': 'Add targets',
'snippets.targets.allHosts': 'All hosts',
'snippets.targets.allHostsShort': 'All hosts',
'snippets.targets.allHostsActive': 'Every host',
}[key] ?? key);
test('renders dynamic group targets separately from explicit hosts', () => {
const markup = renderToStaticMarkup(
<SnippetTargetsSection
t={t}
targetHosts={[]}
targetGroups={['Production/Web']}
onEditTargets={() => undefined}
onEditGroups={() => undefined}
/>,
);
assert.match(markup, /Production\/Web/);
assert.match(markup, /Select hosts/);
assert.match(markup, /Select groups/);
});
test('all-host mode hides host and group pickers', () => {
const markup = renderToStaticMarkup(
<SnippetTargetsSection
t={t}
targetHosts={[]}
targetGroups={['Production']}
onEditTargets={() => undefined}
onEditGroups={() => undefined}
targetsAllHosts
onTargetsAllHostsChange={() => undefined}
/>,
);
assert.doesNotMatch(markup, /Select hosts/);
assert.doesNotMatch(markup, /Select groups/);
assert.match(markup, /Every host/);
});

View File

@@ -0,0 +1,199 @@
import { FolderTree, Server } from 'lucide-react';
import React from 'react';
import { DistroAvatar } from '../DistroAvatar';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { hostDisplayTitle } from '@/domain/hostDisplay.ts';
import { cn } from '@/lib/utils';
import type { Host } from '@/domain/models';
export interface SnippetTargetsSectionProps {
t: (key: string, params?: Record<string, unknown>) => string;
targetHosts: Host[];
onEditTargets: () => void;
targetGroups?: string[];
onEditGroups?: () => void;
hint?: string;
variant?: 'card' | 'embedded';
targetsAllHosts?: boolean;
onTargetsAllHostsChange?: (checked: boolean) => void;
}
const actionButtonClass = (embedded: boolean) => cn(
'shrink-0 rounded-md transition-colors',
embedded ? 'h-6 px-2 text-[11px]' : 'h-6 px-2 text-xs',
);
const TargetsBody: React.FC<{
t: SnippetTargetsSectionProps['t'];
targetHosts: Host[];
onEditTargets: () => void;
targetGroups?: string[];
onEditGroups?: () => void;
hint?: string;
embedded?: boolean;
targetsAllHosts?: boolean;
onTargetsAllHostsChange?: (checked: boolean) => void;
}> = ({
t,
targetHosts,
onEditTargets,
targetGroups = [],
onEditGroups,
hint,
embedded = false,
targetsAllHosts = false,
onTargetsAllHostsChange,
}) => (
<>
<div className="flex items-center justify-between gap-3">
<p className={cn(
'font-semibold text-muted-foreground shrink-0',
embedded ? 'text-[11px]' : 'text-xs',
)}
>
{t('snippets.targets.title')}
</p>
<div className="flex items-center gap-1 min-w-0">
{!targetsAllHosts ? (
<>
<Button
variant="ghost"
size="sm"
className={cn(actionButtonClass(embedded), 'text-primary gap-1')}
onClick={onEditTargets}
>
<Server size={12} />
{t('snippets.targets.selectHosts')}
</Button>
{onEditGroups ? (
<Button
variant="ghost"
size="sm"
className={cn(actionButtonClass(embedded), 'text-primary gap-1')}
onClick={onEditGroups}
>
<FolderTree size={12} />
{t('snippets.targets.selectGroups')}
</Button>
) : null}
</>
) : null}
{onTargetsAllHostsChange ? (
<button
type="button"
aria-pressed={targetsAllHosts}
aria-label={t('snippets.targets.allHosts')}
className={cn(
actionButtonClass(embedded),
targetsAllHosts
? 'bg-muted text-foreground font-medium'
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground',
)}
onClick={() => onTargetsAllHostsChange(!targetsAllHosts)}
>
{t('snippets.targets.allHostsShort')}
</button>
) : null}
</div>
</div>
{hint && !targetsAllHosts ? (
<p className="text-[11px] text-muted-foreground leading-relaxed">{hint}</p>
) : null}
{targetsAllHosts ? (
<p className={cn(
'text-muted-foreground/80',
embedded ? 'text-[10px]' : 'text-[11px]',
)}
>
{t('snippets.targets.allHostsActive')}
</p>
) : targetHosts.length === 0 && targetGroups.length === 0 ? (
embedded ? null : (
<Button
variant="secondary"
className="w-full h-10"
onClick={onEditTargets}
>
{t('snippets.targets.add')}
</Button>
)
) : (
<div className={cn('gap-2', embedded ? 'flex flex-wrap' : 'space-y-2')}>
{targetGroups.map((groupPath) => (
<div
key={`group:${groupPath}`}
className={cn(
'flex items-center gap-2 rounded-lg border border-border/60 bg-primary/5 max-w-full',
embedded ? 'px-2 py-1.5 text-xs' : 'px-3 py-2 text-sm',
)}
>
<FolderTree size={14} className="text-primary shrink-0" />
<span className="truncate font-medium">{groupPath}</span>
</div>
))}
{targetHosts.map((host) => (
embedded ? (
<div
key={host.id}
className="flex items-center gap-2 px-2 py-1.5 rounded-lg border border-border/60 bg-background/60 text-xs max-w-full"
>
<DistroAvatar host={host} fallback={host.os[0].toUpperCase()} size="sm" />
<span className="truncate font-medium">{hostDisplayTitle(host)}</span>
</div>
) : (
<div
key={host.id}
className="flex items-center gap-3 px-3 py-2 bg-background/60 border border-border/70 rounded-lg"
>
<DistroAvatar host={host} fallback={host.os[0].toUpperCase()} size="log" />
<div className="min-w-0 flex-1 text-sm font-semibold truncate">
{hostDisplayTitle(host)}
</div>
</div>
)
))}
</div>
)}
</>
);
export const SnippetTargetsSection: React.FC<SnippetTargetsSectionProps> = ({
t,
targetHosts,
onEditTargets,
targetGroups,
onEditGroups,
hint,
variant = 'card',
targetsAllHosts,
onTargetsAllHostsChange,
}) => {
const body = (
<TargetsBody
t={t}
targetHosts={targetHosts}
onEditTargets={onEditTargets}
targetGroups={targetGroups}
onEditGroups={onEditGroups}
hint={hint}
embedded={variant === 'embedded'}
targetsAllHosts={targetsAllHosts}
onTargetsAllHostsChange={onTargetsAllHostsChange}
/>
);
if (variant === 'embedded') {
return <div className="space-y-2">{body}</div>;
}
return (
<Card className="p-3 space-y-3 bg-card border-border/80">
{body}
</Card>
);
};