chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

-63
View File
@@ -1,63 +0,0 @@
import React from "react";
import { cn } from "@/lib/utils.ts";
interface SimpleLoaderProps {
visible: boolean;
message?: string;
className?: string;
backgroundColor?: string;
}
export function SimpleLoader({
visible,
message,
className,
backgroundColor,
}: SimpleLoaderProps) {
if (!visible) {
return null;
}
return (
<>
<style>
{`
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.simple-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-base);
border-top-color: var(--foreground);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
`}
</style>
<div
className={cn(
"absolute inset-0 flex items-center justify-center z-[100]",
className,
)}
style={{ backgroundColor: backgroundColor || "var(--bg-base)" }}
>
<div className="flex flex-col items-center gap-4">
<div className="simple-spinner"></div>
{message && (
<p className="text-sm text-foreground-secondary font-medium">
{message}
</p>
)}
</div>
</div>
</>
);
}
+29 -2
View File
@@ -6,7 +6,15 @@ import {
getCredentialSidebarPreferences,
saveCredentialSidebarPreferences,
} from "@/main-axios";
import { PRESETS, type UiPreset } from "@/types/ui-preferences";
import {
PRESETS,
type UiPreset,
type UiRailPreferences,
} from "@/types/ui-preferences";
import {
listRegisteredRailItems,
type RailItemDef,
} from "@/sidebar/rail-items";
import { sanitizeHostSidebarPreferences } from "@/types/host-sidebar-preferences";
import { sanitizeCredentialSidebarPreferences } from "@/types/credential-sidebar-preferences";
import { getRegisteredDashboardCard } from "@/dashboard/dashboard-cards-registry";
@@ -68,6 +76,23 @@ function writeLocal(key: string, value: string) {
}
}
/**
* The rail views a preset hides: its own list, plus every plugin item that
* does not ask to stay when the preset hides plugin items.
*/
export function presetHiddenRailTabs(
rail: UiRailPreferences,
pluginItems: Pick<RailItemDef, "id" | "simplePreset" | "hideable">[],
): string[] {
if (!rail.hidePluginItems) return [...rail.hiddenTabs];
return [
...rail.hiddenTabs,
...pluginItems
.filter((item) => !item.simplePreset && item.hideable !== false)
.map((item) => item.id),
];
}
/**
* Writes a preset's values into the stores that own them. Safe to call when
* nothing changed; every write is idempotent.
@@ -87,7 +112,9 @@ export async function applyPresetSideEffects(
// Rail visibility lives on user_preferences and is mirrored to localStorage
// with a change event, the same way the settings toggles write it.
const hiddenRailTabs = JSON.stringify(target.rail.hiddenTabs);
const hiddenRailTabs = JSON.stringify(
presetHiddenRailTabs(target.rail, listRegisteredRailItems()),
);
writeLocal("hiddenRailTabs", hiddenRailTabs);
window.dispatchEvent(new Event("hiddenRailTabsChanged"));
if (isCloud) {
+9 -2
View File
@@ -373,7 +373,14 @@ export const authLogger = new FrontendLogger("AUTH", "🔐", "#dc2626");
export const sshLogger = new FrontendLogger("SSH", "🖥️", "#1e3a8a");
export const fileLogger = new FrontendLogger("FILE", "📁", "#1e3a8a");
export const statsLogger = new FrontendLogger("STATS", "📊", "#22c55e");
export const systemLogger = new FrontendLogger("SYSTEM", "🚀", "#1e3a8a");
const systemLogger = new FrontendLogger("SYSTEM", "🚀", "#1e3a8a");
export const dashboardLogger = new FrontendLogger("DASHBOARD", "📊", "#ec4899");
export const logger = systemLogger;
/** A named console logger, for plugins that want the same log format. */
export function createFrontendLogger(
serviceName: string,
serviceIcon = "🧩",
serviceColor = "#6b7280",
) {
return new FrontendLogger(serviceName, serviceIcon, serviceColor);
}
-4
View File
@@ -70,10 +70,6 @@ export function getCachedServerStatuses(
return statusCache.get(loader);
}
export function invalidateSSHHostsCache(): void {
hostsCache.invalidate();
}
export function invalidateServerStatusCache(): void {
statusCache.invalidate();
}
-22
View File
@@ -1,22 +0,0 @@
/**
* The shape an axios rejection actually has at the call sites that inspect it.
* A `catch` binding is `unknown`, and every one of these sites was reaching
* into it directly; this gives them one narrowing helper instead.
*/
export interface HttpLikeError {
message?: string;
code?: string;
response?: {
status?: number;
data?: {
error?: string;
message?: string;
code?: string;
fileNotFound?: boolean;
};
};
}
export function asHttpError(error: unknown): HttpLikeError {
return (error ?? {}) as HttpLikeError;
}
-82
View File
@@ -1,82 +0,0 @@
export interface KeyedRequestCache<T> {
get(
key: string,
loader: () => Promise<T>,
options?: { force?: boolean },
): Promise<T>;
peek(key: string, maxAgeMs?: number): T | null;
set(key: string, value: T): void;
invalidate(key?: string): void;
}
/** Bounded per-key cache with in-flight request coalescing. */
export function createKeyedRequestCache<T>(
ttlMs: number,
maxEntries = 100,
): KeyedRequestCache<T> {
const values = new Map<string, { value: T; storedAt: number }>();
const inflight = new Map<string, Promise<T>>();
const versions = new Map<string, number>();
const store = (key: string, value: T) => {
values.delete(key);
values.set(key, { value, storedAt: Date.now() });
while (values.size > Math.max(1, maxEntries)) {
const oldest = values.keys().next().value;
if (oldest === undefined) break;
values.delete(oldest);
}
};
return {
get(key, loader, options = {}) {
const cached = values.get(key);
if (!options.force && cached && Date.now() - cached.storedAt < ttlMs) {
return Promise.resolve(cached.value);
}
const pending = inflight.get(key);
if (pending) return pending;
const version = versions.get(key) ?? 0;
const request = loader()
.then((value) => {
if ((versions.get(key) ?? 0) === version) store(key, value);
return value;
})
.finally(() => {
if (inflight.get(key) === request) inflight.delete(key);
});
inflight.set(key, request);
return request;
},
peek(key, maxAgeMs = Number.POSITIVE_INFINITY) {
const cached = values.get(key);
if (!cached || Date.now() - cached.storedAt > maxAgeMs) return null;
values.delete(key);
values.set(key, cached);
return cached.value;
},
set(key, value) {
versions.set(key, (versions.get(key) ?? 0) + 1);
inflight.delete(key);
store(key, value);
},
invalidate(key) {
if (key === undefined) {
for (const activeKey of inflight.keys()) {
versions.set(activeKey, (versions.get(activeKey) ?? 0) + 1);
}
values.clear();
inflight.clear();
} else {
versions.set(key, (versions.get(key) ?? 0) + 1);
values.delete(key);
inflight.delete(key);
}
},
};
}
-105
View File
@@ -1,105 +0,0 @@
// Thin, typed access to the desktop app's local filesystem bridge
// (electron/local-files.cjs via preload.js). Everything here resolves to
// "unavailable" outside Electron so callers can feature-detect cheaply.
import type {
LocalDirectoryListing,
LocalFsHomeInfo,
LocalFsResult,
LocalTrashResult,
LocalWalkResult,
} from "@/types/electron";
import { isElectron } from "./electron";
export function isLocalFileBrowserAvailable(): boolean {
if (!isElectron()) return false;
const api = window.electronAPI;
return !!api?.localFs && !!api?.localTransfer;
}
function requireLocalFs() {
const api = window.electronAPI?.localFs;
if (!api) {
throw new Error("Local file access is only available in the desktop app");
}
return api;
}
function unwrap<T>(result: LocalFsResult<T>): T {
if (!result || result.success !== true) {
const message =
result && "error" in result && result.error
? result.error
: "Local file operation failed";
const error = new Error(message) as Error & { code?: string };
if (result && "code" in result) error.code = result.code;
throw error;
}
return result;
}
export async function getLocalHome(): Promise<LocalFsHomeInfo> {
return unwrap(await requireLocalFs().home());
}
export async function listLocalDirectory(
dirPath: string,
): Promise<LocalDirectoryListing> {
return unwrap(await requireLocalFs().list(dirPath));
}
export async function createLocalFolder(
parentPath: string,
name: string,
): Promise<string> {
return unwrap(await requireLocalFs().mkdir(parentPath, name)).path;
}
export async function createLocalFile(
parentPath: string,
name: string,
): Promise<string> {
return unwrap(await requireLocalFs().createFile(parentPath, name)).path;
}
export async function renameLocalEntry(
oldPath: string,
newName: string,
): Promise<string> {
return unwrap(await requireLocalFs().rename(oldPath, newName)).path;
}
/** Moves entries to the OS trash; resolves with per-path failures, if any. */
export async function trashLocalPaths(
paths: string[],
): Promise<LocalTrashResult> {
return unwrap(await requireLocalFs().trash(paths));
}
/** Subset of `paths` that already exist on disk. */
export async function localPathsExist(paths: string[]): Promise<string[]> {
if (paths.length === 0) return [];
return unwrap(await requireLocalFs().exists(paths)).existing;
}
/** Creates `dirPath` (and parents). With `rootPath`, refuses anything outside it. */
export async function ensureLocalDirectory(
dirPath: string,
rootPath?: string,
): Promise<string> {
return unwrap(await requireLocalFs().ensureDir(dirPath, rootPath)).path;
}
export async function walkLocalPaths(
paths: string[],
): Promise<LocalWalkResult> {
return unwrap(await requireLocalFs().walk(paths));
}
export async function revealLocalPath(targetPath: string): Promise<void> {
unwrap(await requireLocalFs().reveal(targetPath));
}
export async function openLocalPath(targetPath: string): Promise<void> {
unwrap(await requireLocalFs().open(targetPath));
}
+1 -18
View File
@@ -31,7 +31,7 @@ import {
/** The backend port. Everything plugin-facing rides the main server. */
const BACKEND_PORT = 30001;
export function pluginApiPath(pluginId: string, path = ""): string {
function pluginApiPath(pluginId: string, path = ""): string {
const suffix = path && !path.startsWith("/") ? `/${path}` : path;
return `/plugin-api/${pluginId}${suffix}`;
}
@@ -123,23 +123,6 @@ export function pluginApiFor(
return client;
}
/**
* pluginWsUrl for a path a backend handed out ("/plugin-ws/<id>/<path>?q"),
* so a caller can dial a plugin's socket without naming the plugin.
*/
export async function pluginWsUrlForPath(
wsPath: string,
options: { origin?: ConnectionOrigin } = {},
): Promise<WebSocketConnectionTarget | null> {
const [pathname, query] = wsPath.split("?", 2);
const match = /^\/plugin-ws\/([^/]+)(\/.*)$/.exec(pathname);
if (!match) return null;
const target = await pluginWsUrl(match[1], match[2], options);
if (!target || !query) return target;
const separator = target.url.includes("?") ? "&" : "?";
return { ...target, url: `${target.url}${separator}${query}` };
}
/**
* The WebSocket URL for /plugin-ws/<id>/<path>, plus the subprotocols that
* carry the JWT.
+1 -1
View File
@@ -23,7 +23,7 @@ export interface SharedHostConnectionAuth {
keyType?: string | null;
}
export async function getRemoteSharedHostConnectionAuth(
async function getRemoteSharedHostConnectionAuth(
localHostId: number,
): Promise<SharedHostConnectionAuth> {
if (localHostId >= 0) {
+22
View File
@@ -0,0 +1,22 @@
import { invokeAction } from "@/shell/action-registry";
/** A snippet as core's pickers show it. */
export interface SnippetRow {
id: number;
name: string;
content: string;
folder?: string | null;
isNote?: boolean;
}
/**
* The user's snippets, from whichever plugin provides them through the
* "snippet.list" action. Empty while no plugin does. An admin editing
* another user's host passes that user's id.
*/
export async function listSnippets(options?: {
targetUserId?: string;
}): Promise<SnippetRow[]> {
const rows = await invokeAction("snippet.list", options);
return Array.isArray(rows) ? (rows as SnippetRow[]) : [];
}
-5
View File
@@ -27,11 +27,6 @@ export function extractSnippetInputs(content: string): SnippetInput[] {
return Array.from(seen.values());
}
export function hasSnippetInputs(content: string): boolean {
INPUT_PATTERN.lastIndex = 0;
return INPUT_PATTERN.test(content);
}
function replaceVar(
content: string,
name: string,
-5
View File
@@ -5,11 +5,6 @@ export const splitDragState = { active: false };
// Callbacks registered by terminal instances to trigger a fit after drag ends.
const fitCallbacks = new Set<() => void>();
export function registerFitCallback(fn: () => void) {
fitCallbacks.add(fn);
return () => fitCallbacks.delete(fn);
}
export function notifyDragEnd() {
for (const fn of fitCallbacks) fn();
}