feat: convert file manager into a plugin

This commit is contained in:
LukeGus committed 2026-09-23 16:27:29 -05:00
1 parent b76da9ab52
commit c85c1fdb86
259 files changed
+54155 -16791

No files matched your search

+1 -6
View File
@@ -1,5 +1,5 @@
import type { ComponentType } from "react";
import { FolderOpen, Network } from "lucide-react";
import { Network } from "lucide-react";
import { tabTypeForActivity } from "@/shell/tab-registry";
export interface ActivityTarget {
@@ -11,11 +11,6 @@ export interface ActivityTarget {
/** Recent-activity types core itself records. */
const CORE_ACTIVITY: Record<string, ActivityTarget> = {
file_manager: {
icon: FolderOpen,
tab: "files",
labelKey: "dashboard.activityFileManager",
},
tunnel: {
icon: Network,
tab: "tunnel",
-38
View File
@@ -1,38 +0,0 @@
import type { FileItem } from "@/types/index";
import { createKeyedRequestCache } from "./keyed-request-cache";
export interface FileContentResult {
content: string;
path: string;
encoding?: "base64" | "utf8";
}
const cache = createKeyedRequestCache<FileContentResult>(30_000, 24);
const keyFor = (sessionId: string, path: string) => `${sessionId}\0${path}`;
export function getCachedFileContent(
sessionId: string,
path: string,
loader: () => Promise<FileContentResult>,
force = false,
): Promise<FileContentResult> {
return cache.get(keyFor(sessionId, path), loader, { force });
}
export function invalidateCachedFileContent(
sessionId: string,
path: string,
): void {
cache.invalidate(keyFor(sessionId, path));
}
export function shouldPrefetchFileContent(
file: FileItem,
maxPrefetchBytes: number,
): boolean {
return (
file.type === "file" &&
typeof file.size === "number" &&
file.size <= maxPrefetchBytes
);
}
-43
View File
@@ -1,43 +0,0 @@
import type { FileItem } from "@/types/index";
import { createKeyedRequestCache } from "./keyed-request-cache";
export interface FileListResult {
files: FileItem[];
path: string;
}
const cache = createKeyedRequestCache<FileListResult>(5_000, 150);
const keyFor = (sessionId: string, path: string) => `${sessionId}\0${path}`;
export function getCachedFileList(
sessionId: string,
path: string,
loader: () => Promise<FileListResult>,
force = false,
): Promise<FileListResult> {
return cache.get(keyFor(sessionId, path), loader, { force });
}
export function peekCachedFileList(
sessionId: string,
path: string,
maxAgeMs = 30_000,
): FileListResult | null {
return cache.peek(keyFor(sessionId, path), maxAgeMs);
}
export function invalidateCachedFileList(
sessionId: string,
path: string,
): void {
cache.invalidate(keyFor(sessionId, path));
}
export function updateCachedFileList(
sessionId: string,
path: string,
files: FileItem[],
): void {
cache.set(keyFor(sessionId, path), { files, path });
}
-191
View File
@@ -1,191 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { ConnectionStatus } from "@/components/connection/connection-status.ts";
interface UseConnectionRetryOptions {
connect: () => void | Promise<void>;
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
enabled?: boolean;
autoStart?: boolean;
}
interface UseConnectionRetryResult {
status: ConnectionStatus;
attempt: number;
maxAttempts: number;
nextRetryInMs: number | null;
markConnected: () => void;
markFailed: () => void;
retryNow: () => void;
reset: () => void;
}
const COUNTDOWN_TICK_MS = 250;
const RETRY_JITTER_RATIO = 0.1;
export function computeReconnectDelay(
attempt: number,
baseDelayMs: number,
maxDelayMs: number,
random = Math.random,
): number {
const base = Math.min(
baseDelayMs * Math.pow(2, Math.max(0, attempt - 1)),
maxDelayMs,
);
const jitter = base * RETRY_JITTER_RATIO * (random() * 2 - 1);
return Math.max(baseDelayMs, Math.min(maxDelayMs, Math.round(base + jitter)));
}
// Backoff curve mirrors Terminal.tsx's proven attemptReconnection(): 2s, 4s,
// 8s, capped at 8s, up to 8 attempts before falling back to a manual retry.
export function useConnectionRetry({
connect,
maxAttempts = 8,
baseDelayMs = 2000,
maxDelayMs = 8000,
enabled = true,
autoStart = true,
}: UseConnectionRetryOptions): UseConnectionRetryResult {
const [status, setStatus] = useState<ConnectionStatus>("connecting");
const [attempt, setAttempt] = useState(0);
const [nextRetryInMs, setNextRetryInMs] = useState<number | null>(null);
const connectRef = useRef(connect);
connectRef.current = connect;
const enabledRef = useRef(enabled);
enabledRef.current = enabled;
const attemptRef = useRef(0);
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const countdownIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null,
);
const isMountedRef = useRef(true);
const markFailedRef = useRef<() => void>(() => {});
const clearTimers = useCallback(() => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
}, []);
const runConnect = useCallback(() => {
if (!isMountedRef.current) return;
setStatus("connecting");
try {
void Promise.resolve(connectRef.current()).catch(() => {
markFailedRef.current();
});
} catch {
markFailedRef.current();
}
}, []);
const scheduleRetry = useCallback(() => {
if (attemptRef.current >= maxAttempts) {
setStatus("disconnected");
setNextRetryInMs(null);
return;
}
attemptRef.current += 1;
setAttempt(attemptRef.current);
const delay = computeReconnectDelay(
attemptRef.current,
baseDelayMs,
maxDelayMs,
);
let remaining = delay;
setNextRetryInMs(remaining);
countdownIntervalRef.current = setInterval(() => {
remaining = Math.max(0, remaining - COUNTDOWN_TICK_MS);
setNextRetryInMs(remaining);
}, COUNTDOWN_TICK_MS);
retryTimeoutRef.current = setTimeout(() => {
clearTimers();
if (!isMountedRef.current || !enabledRef.current) return;
runConnect();
}, delay);
}, [baseDelayMs, maxDelayMs, maxAttempts, clearTimers, runConnect]);
const markConnected = useCallback(() => {
clearTimers();
attemptRef.current = 0;
setAttempt(0);
setNextRetryInMs(null);
if (isMountedRef.current) setStatus("connected");
}, [clearTimers]);
const markFailed = useCallback(() => {
clearTimers();
if (!isMountedRef.current) return;
setStatus("error");
if (enabledRef.current) {
scheduleRetry();
} else {
setNextRetryInMs(null);
}
}, [clearTimers, scheduleRetry]);
markFailedRef.current = markFailed;
const retryNow = useCallback(() => {
clearTimers();
attemptRef.current = 0;
setAttempt(0);
setNextRetryInMs(null);
runConnect();
}, [clearTimers, runConnect]);
const reset = useCallback(() => {
clearTimers();
attemptRef.current = 0;
setAttempt(0);
setNextRetryInMs(null);
if (isMountedRef.current) setStatus("connecting");
}, [clearTimers]);
useEffect(() => {
isMountedRef.current = true;
if (autoStart) {
runConnect();
}
return () => {
isMountedRef.current = false;
clearTimers();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (enabled && status === "error" && !retryTimeoutRef.current) {
scheduleRetry();
}
if (!enabled) {
clearTimers();
setNextRetryInMs(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled]);
return {
status,
attempt,
maxAttempts,
nextRetryInMs,
markConnected,
markFailed,
retryNow,
reset,
};
}