[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,255 @@
import { Plus, TerminalSquare } from 'lucide-react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useI18n } from '../../application/i18n/I18nProvider';
import type { useSystemManagerBackend } from '../../application/state/useSystemManagerBackend';
import type { Snippet, TerminalSession } from '../../types';
import type { TmuxClientInfo, TmuxSessionInfo, TmuxWindowInfo } from '../../domain/systemManager/types';
import { tmuxSessionInfoEqual } from '../../domain/systemManager/pollEquals';
import {
SystemPanelEmpty,
SystemPanelError,
SystemPanelIconButton,
SystemPanelList,
SystemPanelLoading,
SystemPanelMetaBar,
SystemPanelRefreshButton,
SystemPanelSearch,
SystemPanelShell,
SystemPanelToolbar,
} from './SystemPanelUi';
import { useAsyncRecordCache } from '../../application/state/systemManager/useAsyncRecordCache';
import { usePolling, useStableTranslate } from '../../application/state/useSystemManager';
import { TmuxNewSessionModal } from './TmuxNewSessionModal';
import { TmuxSessionCard } from './TmuxSessionCard';
import { useStableListOrder, mergePollListByKey } from './listStable';
type Backend = ReturnType<typeof useSystemManagerBackend>;
export interface TmuxSessionDetails {
windows: TmuxWindowInfo[];
clients: TmuxClientInfo[];
}
interface TmuxManagerTabProps {
sessionId: string;
parentSession: TerminalSession;
isVisible: boolean;
warmupEnabled?: boolean;
backend: Backend;
refreshIntervalSec: number;
snippets: Snippet[];
onRequestTerminalFocus?: () => void;
}
export const TmuxManagerTab = memo(function TmuxManagerTab({
sessionId,
parentSession,
isVisible,
warmupEnabled = false,
backend,
refreshIntervalSec,
snippets,
onRequestTerminalFocus,
}: TmuxManagerTabProps) {
const { t } = useI18n();
const stableT = useStableTranslate();
const [query, setQuery] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [modalError, setModalError] = useState<string | null>(null);
const [tmuxVersion, setTmuxVersion] = useState<string | null>(null);
const currentSessionIdRef = useRef(sessionId);
currentSessionIdRef.current = sessionId;
useEffect(() => {
setTmuxVersion(null);
}, [sessionId]);
const fetcher = useCallback(async () => {
const fetchSessionId = sessionId;
const result = await backend.listTmuxSessions(sessionId);
const version = result.tmuxVersion ?? null;
if (currentSessionIdRef.current === fetchSessionId) {
setTmuxVersion((prev) => (prev === version ? prev : version));
}
if (!result.success) {
throw new Error(result.error || stableT('systemManager.errors.loadTmux'));
}
return result.sessions ?? [];
}, [backend, sessionId, stableT]);
const intervalMs = Math.max(2, refreshIntervalSec) * 1000;
const { data: sessions, error, loading, refresh } = usePolling<TmuxSessionInfo[]>(
fetcher,
intervalMs,
isVisible || warmupEnabled,
(prev, next) => mergePollListByKey(prev, next, (s) => s.name, tmuxSessionInfoEqual),
{ poll: isVisible, resetKey: sessionId },
);
const filtered = useMemo<TmuxSessionInfo[]>(() => {
const q = query.trim().toLowerCase();
const list = sessions ?? [];
if (!q) return list;
return list.filter((session) => session.name.toLowerCase().includes(q));
}, [query, sessions]);
const compareSessions = useCallback(
(a: TmuxSessionInfo, b: TmuxSessionInfo) => a.name.localeCompare(b.name),
[],
);
const displaySessions = useStableListOrder<TmuxSessionInfo, string>(
filtered,
(s) => s.name,
query,
compareSessions,
);
const formatTmuxLoadError = useCallback((
message: string,
debug?: { lastOutput?: string; tried?: string[] },
) => {
const parts = [message];
if (debug?.lastOutput) parts.push(debug.lastOutput);
if (debug?.tried?.length) {
parts.push(t('systemManager.tmux.lastCommand', { command: debug.tried[debug.tried.length - 1] ?? '' }));
}
return parts.filter(Boolean).join(' · ');
}, [t]);
const getTmuxDetailsKey = useCallback((session: TmuxSessionInfo) => (
`${sessionId}:${session.name}:${session.created}`
), [sessionId]);
const fetchTmuxDetails = useCallback(async (session: TmuxSessionInfo): Promise<TmuxSessionDetails> => {
const [windowsResult, clientsResult] = await Promise.all([
backend.listTmuxWindows({ sessionId, sessionName: session.name }),
backend.listTmuxClients({ sessionId, sessionName: session.name }),
]);
if (!windowsResult.success) {
throw new Error(formatTmuxLoadError(
windowsResult.error || stableT('systemManager.errors.loadTmuxWindows'),
windowsResult.debug,
));
}
if (!clientsResult.success) {
throw new Error(clientsResult.error || stableT('systemManager.errors.loadTmuxClients'));
}
const freshWindows = windowsResult.windows ?? [];
if (freshWindows.length === 0 && session.windows > 0) {
throw new Error(formatTmuxLoadError(
stableT('systemManager.tmux.windowsMismatch', { count: String(session.windows) }),
windowsResult.debug,
));
}
return {
windows: freshWindows,
clients: clientsResult.clients ?? [],
};
}, [backend, formatTmuxLoadError, sessionId, stableT]);
const {
records: tmuxDetailsByName,
loadRecord: loadTmuxDetails,
refreshRecord: refreshTmuxDetails,
} = useAsyncRecordCache<TmuxSessionInfo, TmuxSessionDetails>({
items: sessions ?? [],
enabled: isVisible && (sessions?.length ?? 0) > 0,
getKey: getTmuxDetailsKey,
fetchRecord: fetchTmuxDetails,
prefetchLimit: 16,
prefetchDelayMs: 40,
staleTimeMs: 20_000,
});
const handleCreate = useCallback(async (name: string, command: string) => {
setCreating(true);
setModalError(null);
try {
const result = await backend.createTmuxSession({
sessionId,
name,
command: command || undefined,
});
if (!result.success) throw new Error(result.error);
setModalOpen(false);
await refresh();
} catch (err) {
setModalError(err instanceof Error ? err.message : t('systemManager.errors.actionFailed'));
} finally {
setCreating(false);
}
}, [backend, refresh, sessionId, t]);
return (
<SystemPanelShell section="system-manager-tmux">
<SystemPanelToolbar
trailing={(
<>
<SystemPanelIconButton
title={t('systemManager.tmux.new')}
onClick={() => {
setModalError(null);
setModalOpen(true);
}}
>
<Plus size={14} />
</SystemPanelIconButton>
<SystemPanelRefreshButton
title={t('history.action.refresh')}
loading={loading}
onClick={() => void refresh()}
/>
</>
)}
>
<SystemPanelSearch
value={query}
onChange={setQuery}
placeholder={t('systemManager.tmux.search')}
/>
</SystemPanelToolbar>
<SystemPanelMetaBar trailing={tmuxVersion ? (
<span className="shrink-0 text-[10px] text-muted-foreground">{tmuxVersion}</span>
) : undefined}>
{t('systemManager.tmux.meta', { count: displaySessions.length })}
</SystemPanelMetaBar>
<SystemPanelList>
{!error && displaySessions.length === 0 && loading && (
<SystemPanelLoading message={t('systemManager.common.loading')} />
)}
{!error && displaySessions.length === 0 && !loading && (
<SystemPanelEmpty icon={TerminalSquare} message={t('systemManager.tmux.empty')} />
)}
{error && (
<SystemPanelError message={error} onRetry={() => void refresh()} retryLabel={t('history.action.retry')} loading={loading} />
)}
{displaySessions.map((session) => (
<TmuxSessionCard
key={`${session.name}:${session.created}`}
session={session}
sessionId={sessionId}
parentSession={parentSession}
backend={backend}
detailsRecord={tmuxDetailsByName[getTmuxDetailsKey(session)]}
onLoadDetails={loadTmuxDetails}
onRefreshDetails={refreshTmuxDetails}
onSessionsChanged={refresh}
onRequestTerminalFocus={onRequestTerminalFocus}
/>
))}
</SystemPanelList>
<TmuxNewSessionModal
open={modalOpen}
onOpenChange={setModalOpen}
onCreate={handleCreate}
snippets={snippets}
creating={creating}
error={modalError}
/>
</SystemPanelShell>
);
});