[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
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:
247
application/state/usePluginContributions.ts
Normal file
247
application/state/usePluginContributions.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
||||
import {
|
||||
getSharedPluginRuntimeStatus,
|
||||
invalidateSharedPluginRuntimeStatus,
|
||||
} from './pluginRuntimeStatusCache';
|
||||
|
||||
const EMPTY_SNAPSHOT: NetcattyPluginContributionSnapshot = Object.freeze({
|
||||
locale: 'en',
|
||||
plugins: Object.freeze([]),
|
||||
});
|
||||
|
||||
export function resolvePluginContributionLoadState({
|
||||
currentQueryKey,
|
||||
loadedQueryKey,
|
||||
snapshot,
|
||||
available,
|
||||
loading,
|
||||
}: {
|
||||
currentQueryKey: string;
|
||||
loadedQueryKey: string;
|
||||
snapshot: NetcattyPluginContributionSnapshot;
|
||||
available: boolean;
|
||||
loading: boolean;
|
||||
}): Pick<UsePluginContributionsResult, 'available' | 'loading' | 'snapshot'> {
|
||||
if (currentQueryKey !== loadedQueryKey) {
|
||||
return { available: false, loading: true, snapshot: EMPTY_SNAPSHOT };
|
||||
}
|
||||
return { available, loading, snapshot };
|
||||
}
|
||||
|
||||
export function failClosedPluginContributionLoad(cause: unknown): {
|
||||
available: false;
|
||||
snapshot: NetcattyPluginContributionSnapshot;
|
||||
error: Error;
|
||||
} {
|
||||
return {
|
||||
available: false,
|
||||
snapshot: EMPTY_SNAPSHOT,
|
||||
error: cause instanceof Error ? cause : new Error(String(cause)),
|
||||
};
|
||||
}
|
||||
|
||||
export function comparePluginMenus(
|
||||
left: NetcattyPluginContributionSnapshot['plugins'][number]['menus'][number],
|
||||
right: NetcattyPluginContributionSnapshot['plugins'][number]['menus'][number],
|
||||
): number {
|
||||
return (left.group ?? '').localeCompare(right.group ?? '')
|
||||
|| (left.order ?? 0) - (right.order ?? 0)
|
||||
|| left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
export function collectOwnedPluginMenus(
|
||||
plugins: NetcattyPluginContributionSnapshot['plugins'],
|
||||
) {
|
||||
return plugins.flatMap((plugin) => {
|
||||
const commandById = new Map(plugin.commands.map((command) => [command.id, command] as const));
|
||||
return plugin.menus.map((menu) => ({
|
||||
...menu,
|
||||
pluginId: plugin.id,
|
||||
icon: menu.icon ?? commandById.get(menu.command)?.icon,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function createPluginContributionRefreshGuard() {
|
||||
let generation = 0;
|
||||
return Object.freeze({
|
||||
begin() {
|
||||
const requestGeneration = ++generation;
|
||||
return () => generation === requestGeneration;
|
||||
},
|
||||
invalidate() {
|
||||
generation += 1;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface UsePluginContributionsResult {
|
||||
available: boolean;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
snapshot: NetcattyPluginContributionSnapshot;
|
||||
refresh(): Promise<void>;
|
||||
executeCommand(command: string, args?: unknown, context?: Record<string, unknown>): Promise<unknown>;
|
||||
updateSetting(pluginId: string, settingId: string, value: unknown, scopeId?: string): Promise<{ restartRequired: boolean }>;
|
||||
resetSetting(pluginId: string, settingId: string, scopeId?: string): Promise<{ restartRequired: boolean }>;
|
||||
selectSettingPath(kind: 'file' | 'directory', title: string, defaultPath?: string): Promise<string | null>;
|
||||
openView(payload: NetcattyPluginViewOpenRequest): Promise<{ instanceId: string }>;
|
||||
closeView(instanceId: string): Promise<void>;
|
||||
setViewBounds(instanceId: string, bounds: { x: number; y: number; width: number; height: number }): Promise<void>;
|
||||
setViewVisibility(instanceId: string, visible: boolean): Promise<void>;
|
||||
setEnvironment(environment: NetcattyPluginEnvironment): Promise<void>;
|
||||
onViewClosed(callback: (event: NetcattyPluginViewClosedEvent) => void): () => void;
|
||||
}
|
||||
|
||||
export function usePluginContributions(
|
||||
query: NetcattyPluginContributionQuery = {},
|
||||
options: { enabled?: boolean } = {},
|
||||
): UsePluginContributionsResult {
|
||||
const enabled = options.enabled !== false;
|
||||
const bridge = typeof window === 'undefined' ? undefined : netcattyBridge.get();
|
||||
const queryKey = useMemo(() => JSON.stringify(query), [query]);
|
||||
const [loadedSnapshot, setLoadedSnapshot] = useState(() => ({
|
||||
queryKey,
|
||||
snapshot: EMPTY_SNAPSHOT,
|
||||
}));
|
||||
const [available, setAvailable] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const refreshGuard = useRef(createPluginContributionRefreshGuard());
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const isCurrent = refreshGuard.current.begin();
|
||||
if (!enabled || !bridge?.getPluginRuntimeStatus || !bridge.getPluginContributions) {
|
||||
if (!isCurrent()) return;
|
||||
setAvailable(false);
|
||||
setLoadedSnapshot({ queryKey, snapshot: EMPTY_SNAPSHOT });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const status = await getSharedPluginRuntimeStatus(bridge);
|
||||
if (!isCurrent()) return;
|
||||
setAvailable(status.available);
|
||||
if (!status.available) {
|
||||
setLoadedSnapshot({ queryKey, snapshot: EMPTY_SNAPSHOT });
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
const nextSnapshot = await bridge.getPluginContributions(JSON.parse(queryKey));
|
||||
if (!isCurrent()) return;
|
||||
setLoadedSnapshot({ queryKey, snapshot: nextSnapshot });
|
||||
setError(null);
|
||||
} catch (cause) {
|
||||
if (!isCurrent()) return;
|
||||
const failure = failClosedPluginContributionLoad(cause);
|
||||
setAvailable(failure.available);
|
||||
setLoadedSnapshot({ queryKey, snapshot: failure.snapshot });
|
||||
setError(failure.error);
|
||||
} finally {
|
||||
if (isCurrent()) setLoading(false);
|
||||
}
|
||||
}, [bridge, enabled, queryKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const guard = refreshGuard.current;
|
||||
if (!enabled) return () => guard.invalidate();
|
||||
void refresh();
|
||||
const unsubscribe = bridge?.onPluginContributionsChanged?.(() => {
|
||||
invalidateSharedPluginRuntimeStatus(bridge);
|
||||
void refresh();
|
||||
});
|
||||
return () => {
|
||||
guard.invalidate();
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [bridge, enabled, refresh]);
|
||||
|
||||
const executeCommand = useCallback(async (command: string, args?: unknown, context?: Record<string, unknown>) => {
|
||||
if (!bridge?.executePluginCommand) throw new Error('Plugin commands are unavailable');
|
||||
return bridge.executePluginCommand(command, args, context);
|
||||
}, [bridge]);
|
||||
|
||||
const updateSetting = useCallback(async (
|
||||
pluginId: string,
|
||||
settingId: string,
|
||||
value: unknown,
|
||||
scopeId?: string,
|
||||
) => {
|
||||
if (!bridge?.updatePluginSetting) throw new Error('Plugin settings are unavailable');
|
||||
const result = await bridge.updatePluginSetting(pluginId, settingId, value, scopeId);
|
||||
await refresh();
|
||||
return result;
|
||||
}, [bridge, refresh]);
|
||||
|
||||
const resetSetting = useCallback(async (pluginId: string, settingId: string, scopeId?: string) => {
|
||||
if (!bridge?.resetPluginSetting) throw new Error('Plugin settings are unavailable');
|
||||
const result = await bridge.resetPluginSetting(pluginId, settingId, scopeId);
|
||||
await refresh();
|
||||
return result;
|
||||
}, [bridge, refresh]);
|
||||
|
||||
const selectSettingPath = useCallback(async (kind: 'file' | 'directory', title: string, defaultPath?: string) => {
|
||||
const picker = kind === 'file' ? bridge?.selectFile : bridge?.selectDirectory;
|
||||
if (!picker) throw new Error('Plugin path selection is unavailable');
|
||||
return picker(title, defaultPath);
|
||||
}, [bridge]);
|
||||
|
||||
const openView = useCallback(async (payload: NetcattyPluginViewOpenRequest) => {
|
||||
if (!bridge?.openPluginView) throw new Error('Plugin views are unavailable');
|
||||
return bridge.openPluginView(payload);
|
||||
}, [bridge]);
|
||||
|
||||
const closeView = useCallback(async (instanceId: string) => {
|
||||
if (!bridge?.closePluginView) return;
|
||||
await bridge.closePluginView(instanceId);
|
||||
}, [bridge]);
|
||||
|
||||
const setViewBounds = useCallback(async (
|
||||
instanceId: string,
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
) => {
|
||||
if (!bridge?.setPluginViewBounds) return;
|
||||
await bridge.setPluginViewBounds(instanceId, bounds);
|
||||
}, [bridge]);
|
||||
|
||||
const setViewVisibility = useCallback(async (instanceId: string, visible: boolean) => {
|
||||
if (!bridge?.setPluginViewVisibility) return;
|
||||
await bridge.setPluginViewVisibility(instanceId, visible);
|
||||
}, [bridge]);
|
||||
|
||||
const setEnvironment = useCallback(async (environment: NetcattyPluginEnvironment) => {
|
||||
if (!bridge?.setPluginEnvironment) return;
|
||||
await bridge.setPluginEnvironment(environment);
|
||||
}, [bridge]);
|
||||
|
||||
const onViewClosed = useCallback((callback: (event: NetcattyPluginViewClosedEvent) => void) => (
|
||||
bridge?.onPluginViewClosed?.(callback) ?? (() => {})
|
||||
), [bridge]);
|
||||
|
||||
const currentLoadState = resolvePluginContributionLoadState({
|
||||
currentQueryKey: queryKey,
|
||||
loadedQueryKey: loadedSnapshot.queryKey,
|
||||
snapshot: loadedSnapshot.snapshot,
|
||||
available,
|
||||
loading,
|
||||
});
|
||||
|
||||
return {
|
||||
available: currentLoadState.available,
|
||||
loading: currentLoadState.loading,
|
||||
error,
|
||||
snapshot: currentLoadState.snapshot,
|
||||
refresh,
|
||||
executeCommand,
|
||||
updateSetting,
|
||||
resetSetting,
|
||||
selectSettingPath,
|
||||
openView,
|
||||
closeView,
|
||||
setViewBounds,
|
||||
setViewVisibility,
|
||||
setEnvironment,
|
||||
onViewClosed,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user