[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,140 @@
import React from 'react';
import { ExternalLink } from 'lucide-react';
import { useI18n } from '../../application/i18n/I18nProvider';
import {
pluginAuthenticationChallengeMessage,
usePluginAuthenticationChallenges,
} from '../../application/state/usePluginAuthenticationChallenges';
import { Button } from '../ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
export const PluginAuthenticationHost: React.FC = () => {
const { t } = useI18n();
const state = usePluginAuthenticationChallenges();
const { challenge, busy, externalUrl } = state;
if (!challenge) return null;
const message = pluginAuthenticationChallengeMessage(challenge);
return (
<Dialog open onOpenChange={(open) => { if (!open) void state.complete(undefined, true); }}>
<DialogContent className="sm:max-w-[480px]" hideCloseButton>
<DialogHeader>
<DialogTitle>{challenge.title}</DialogTitle>
<DialogDescription>
{message || t('plugins.authentication.description')}
</DialogDescription>
</DialogHeader>
{state.isText && (
<div className="space-y-2">
<Label htmlFor="plugin-authentication-value">
{challenge.kind === 'otp'
? t('plugins.authentication.code')
: challenge.kind === 'password'
? t('plugins.authentication.password')
: t('plugins.authentication.value')}
</Label>
<Input
id="plugin-authentication-value"
type={challenge.kind === 'password' ? 'password' : 'text'}
autoComplete={challenge.kind === 'password' ? 'current-password' : challenge.kind === 'otp' ? 'one-time-code' : 'off'}
value={state.textValue}
maxLength={8192}
disabled={busy}
onChange={(event) => state.setTextValue(event.target.value)}
onKeyDown={(event) => { if (event.key === 'Enter' && state.canSubmit) state.submit(); }}
autoFocus
/>
</div>
)}
{challenge.kind === 'choice' && (
<div className="max-h-64 space-y-2 overflow-y-auto">
{challenge.choices.map((choice) => {
const selected = state.selectedChoices.includes(choice.id);
return (
<label key={choice.id} className="flex cursor-pointer items-start gap-3 rounded-md border p-3">
<input
type={challenge.multiple ? 'checkbox' : 'radio'}
name="plugin-authentication-choice"
className="mt-0.5 h-4 w-4 accent-primary"
checked={selected}
disabled={busy}
onChange={(event) => state.setChoiceSelected(choice.id, event.target.checked)}
/>
<span className="space-y-1">
<span className="block text-sm font-medium">{choice.label}</span>
{choice.description && <span className="block text-xs text-muted-foreground">{choice.description}</span>}
</span>
</label>
);
})}
</div>
)}
{(challenge.kind === 'browser' || challenge.kind === 'deviceCode') && (
<div className="space-y-3 rounded-md border p-3">
{challenge.kind === 'deviceCode' && (
<div>
<div className="text-xs text-muted-foreground">{t('plugins.authentication.deviceCode')}</div>
<code className="select-all text-base font-semibold">{challenge.userCode}</code>
</div>
)}
{externalUrl ? (
<Button type="button" variant="outline" className="w-full" onClick={() => void state.openExternal()}>
<ExternalLink className="mr-2 h-4 w-4" />
{t('plugins.authentication.openBrowser')}
</Button>
) : (
<p className="text-sm text-destructive">{t('plugins.authentication.invalidUrl')}</p>
)}
</div>
)}
{state.responseError !== null && (
<p role="alert" className="text-sm text-destructive">
{state.responseError
? t('plugins.authentication.responseFailedWithMessage', { message: state.responseError })
: t('plugins.authentication.responseFailed')}
</p>
)}
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={busy}
onClick={() => void (challenge.kind === 'confirmation'
? state.complete(false)
: state.complete(undefined, true))}
>
{challenge.kind === 'confirmation' && challenge.cancelLabel
? challenge.cancelLabel
: t('common.cancel')}
</Button>
{challenge.kind === 'confirmation' ? (
<Button type="button" disabled={busy} onClick={() => void state.complete(true)}>
{challenge.confirmLabel || t('common.confirm')}
</Button>
) : (
<Button type="button" disabled={busy || !state.canSubmit} onClick={state.submit}>
{challenge.kind === 'browser' || challenge.kind === 'deviceCode'
? t('plugins.authentication.continue')
: t('common.confirm')}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,86 @@
import { X } from 'lucide-react';
import { useI18n } from '../../application/i18n/I18nProvider';
import {
requestOpenPluginView,
usePluginViewLifecycle,
} from '../../application/state/usePluginViewLifecycle';
import { Button } from '../ui/button';
import { PluginContributionIcon } from './PluginContributionIcon';
export { requestOpenPluginView };
const DEFAULT_KEYBINDING_CONTEXT = Object.freeze({ 'netcatty.surface': 'keybinding' });
export function PluginContributionHost({
locale,
theme,
themeTokens: suppliedThemeTokens,
keybindingContext = DEFAULT_KEYBINDING_CONTEXT,
}: {
locale: string;
theme: string;
themeTokens?: Record<string, string>;
keybindingContext?: Record<string, unknown>;
}) {
const { t } = useI18n();
const {
activeView,
close,
effectiveRequested,
mountRef,
} = usePluginViewLifecycle({
locale,
theme,
suppliedThemeTokens,
keybindingContext,
});
if (!effectiveRequested || !activeView) return null;
const location = activeView.view.location;
const containerClass = location === 'aside'
? 'absolute inset-y-0 right-0 z-40 w-[420px] border-l border-border bg-background shadow-2xl'
: location === 'panel'
? 'absolute inset-x-0 bottom-0 z-40 h-[42%] border-t border-border bg-background shadow-2xl'
: location === 'modal'
? 'fixed left-1/2 top-1/2 z-50 h-[70vh] w-[min(800px,85vw)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-background shadow-2xl'
: 'absolute inset-0 z-40 bg-background';
if (location === 'tab') {
return (
<section className={`${containerClass} flex flex-col`} role="region" aria-label={activeView.view.title}>
<div ref={mountRef} className="min-h-0 flex-1" />
</section>
);
}
return (
<section
className={`${containerClass} flex flex-col`}
role={location === 'modal' ? 'dialog' : 'region'}
aria-modal={location === 'modal' ? true : undefined}
aria-label={activeView.view.title}
>
<header className="app-no-drag flex h-11 shrink-0 items-center justify-between border-b border-border px-3">
<div className="flex min-w-0 items-center gap-2">
<PluginContributionIcon pluginId={activeView.plugin.id} icon={activeView.view.icon} className="shrink-0" />
<div className="min-w-0">
<div className="truncate text-sm font-medium">{activeView.view.title}</div>
<div className="truncate text-[10px] text-muted-foreground">{activeView.plugin.displayName}</div>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => void close()}
aria-label={t('common.close')}
autoFocus={location === 'modal'}
>
<X size={14} />
</Button>
</header>
<div ref={mountRef} className="min-h-0 flex-1" />
</section>
);
}

View File

@@ -0,0 +1,80 @@
import {
Activity,
Box,
Code,
Command,
FileText,
Folder,
Globe,
Key,
LayoutPanelLeft,
List,
Network,
Palette,
Play,
Puzzle,
Settings,
Shield,
Table,
Terminal,
Wrench,
type LucideIcon,
} from 'lucide-react';
import React from 'react';
import { usePluginContributionIcon } from '../../application/state/usePluginContributionIcon';
import { cn } from '../../lib/utils';
const THEME_ICONS: Readonly<Record<string, LucideIcon>> = Object.freeze({
activity: Activity,
box: Box,
code: Code,
command: Command,
file: FileText,
folder: Folder,
globe: Globe,
key: Key,
'layout-panel': LayoutPanelLeft,
list: List,
network: Network,
palette: Palette,
play: Play,
settings: Settings,
shield: Shield,
table: Table,
terminal: Terminal,
wrench: Wrench,
});
export function PluginContributionIcon({
pluginId,
icon,
size = 14,
className,
}: {
pluginId?: string;
icon?: NetcattyPluginIconReference;
size?: number;
className?: string;
}) {
const packageIcon = usePluginContributionIcon(pluginId, icon);
if (icon?.kind === 'theme') {
const Icon = THEME_ICONS[icon.name] ?? Puzzle;
return <Icon size={size} className={className} aria-hidden="true" data-plugin-icon-kind="theme" />;
}
if (packageIcon) {
return (
<span
className={cn('inline-flex items-center justify-center', className)}
style={{ width: size, height: size }}
aria-hidden="true"
data-plugin-icon-kind="package"
>
<img src={packageIcon.light} alt="" width={size} height={size} className={packageIcon.dark ? 'h-full w-full object-contain dark:hidden' : 'h-full w-full object-contain'} />
{packageIcon.dark && <img src={packageIcon.dark} alt="" width={size} height={size} className="hidden h-full w-full object-contain dark:block" />}
</span>
);
}
return <Puzzle size={size} className={className} aria-hidden="true" data-plugin-icon-kind="fallback" />;
}

View File

@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const pluginConnectionSectionSource = readFileSync(new URL("../PluginConnectionSection.tsx", import.meta.url), "utf8");
const pluginAuthenticationHostSource = readFileSync(new URL("./PluginAuthenticationHost.tsx", import.meta.url), "utf8");
const connectionHookSource = readFileSync(new URL("../../application/state/usePluginConnectionSectionState.ts", import.meta.url), "utf8");
const authenticationHookSource = readFileSync(new URL("../../application/state/usePluginAuthenticationChallenges.ts", import.meta.url), "utf8");
test("plugin connection section delegates provider discovery and credential catalog state to application state", () => {
assert.match(connectionHookSource, /pluginExtensionBridge\.listProviders\("connection"\)/u);
assert.match(connectionHookSource, /pluginExtensionBridge\.onContributionsChanged/u);
assert.match(connectionHookSource, /pluginExtensionBridge\.subscribeCredentialCatalog/u);
assert.doesNotMatch(pluginConnectionSectionSource, /pluginExtensionBridge/u);
});
test("plugin authentication host delegates challenge lifecycle effects to application state", () => {
assert.match(authenticationHookSource, /pluginExtensionBridge\.onAuthenticationChallenge/u);
assert.match(authenticationHookSource, /pluginExtensionBridge\.respondAuthenticationChallenge/u);
assert.match(authenticationHookSource, /catch \(error\) \{\s+setResponseError\(pluginAuthenticationResponseErrorMessage\(error\)\);/u);
assert.doesNotMatch(authenticationHookSource, /catch \{\s+setQueue\(\(existing\) => existing\.filter/u);
assert.match(pluginAuthenticationHostSource, /role="alert"/u);
assert.match(pluginAuthenticationHostSource, /plugins\.authentication\.responseFailed/u);
assert.doesNotMatch(pluginAuthenticationHostSource, /pluginExtensionBridge/u);
});