[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:
278
application/state/usePluginTerminalProviders.ts
Normal file
278
application/state/usePluginTerminalProviders.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { TerminalTheme } from '../../domain/models';
|
||||
import {
|
||||
mergePluginDecorationRules,
|
||||
normalizePluginDecorationResult,
|
||||
normalizePluginThemeResult,
|
||||
type PluginTerminalDecorationRule,
|
||||
type PluginTerminalThemeColors,
|
||||
} from '../../domain/pluginTerminalProviders';
|
||||
import {
|
||||
getWindowPluginTerminalProviderRegistry,
|
||||
PluginTerminalProviderAvailability,
|
||||
type PluginTerminalProviderResponse,
|
||||
} from './pluginTerminalProviderRegistry';
|
||||
|
||||
const ORDINARY_PROVIDER_KINDS = Object.freeze([
|
||||
'terminal.completion',
|
||||
'terminal.decoration',
|
||||
'terminal.link',
|
||||
'terminal.hover',
|
||||
'terminal.matcher',
|
||||
'terminal.semantic',
|
||||
'terminal.prompt',
|
||||
'terminal.background',
|
||||
'terminal.theme',
|
||||
] as const satisfies readonly NetcattyTerminalProviderKind[]);
|
||||
const PROVIDER_DEADLINE_MS = 1_500;
|
||||
const PROVIDER_RESPONSE_TIMEOUT_MS = 1_800;
|
||||
|
||||
export type RequestPluginTerminalProviders = (
|
||||
kind: NetcattyTerminalProviderKind,
|
||||
operation: string,
|
||||
payload: Readonly<Record<string, unknown>>,
|
||||
deadlineMs: number,
|
||||
supersessionKey?: string,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<PluginTerminalProviderResponse>;
|
||||
|
||||
export interface PluginTerminalProviderRefreshMetadata {
|
||||
sessionId: string;
|
||||
hostId?: string;
|
||||
workspaceId?: string;
|
||||
protocol: string;
|
||||
status: NetcattyTerminalSessionSnapshot['status'];
|
||||
shellType?: NetcattyTerminalSessionSnapshot['shellType'];
|
||||
baseTheme: TerminalTheme;
|
||||
}
|
||||
|
||||
interface UsePluginTerminalProvidersOptions extends PluginTerminalProviderRefreshMetadata {
|
||||
getSnapshotState(): Partial<NetcattyTerminalSessionSnapshot>;
|
||||
}
|
||||
|
||||
export function mergePluginTerminalThemeColors(
|
||||
results: readonly PluginTerminalThemeColors[],
|
||||
): PluginTerminalThemeColors {
|
||||
const merged: Record<string, string> = {};
|
||||
for (const result of results) {
|
||||
for (const [key, color] of Object.entries(result)) {
|
||||
if (!(key in merged) && color) merged[key] = color;
|
||||
}
|
||||
}
|
||||
return Object.freeze(merged) as PluginTerminalThemeColors;
|
||||
}
|
||||
|
||||
export function resolvePluginTerminalTheme(
|
||||
baseTheme: TerminalTheme,
|
||||
themeColors: PluginTerminalThemeColors,
|
||||
): TerminalTheme {
|
||||
if (Object.keys(themeColors).length === 0) return baseTheme;
|
||||
return {
|
||||
...baseTheme,
|
||||
id: `${baseTheme.id}:plugin`,
|
||||
colors: { ...baseTheme.colors, ...themeColors },
|
||||
};
|
||||
}
|
||||
|
||||
export async function waitForProviderResponse(
|
||||
providerRequest: Promise<PluginTerminalProviderResponse>,
|
||||
controller: AbortController,
|
||||
timeoutMs = PROVIDER_RESPONSE_TIMEOUT_MS,
|
||||
): Promise<PluginTerminalProviderResponse> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
providerRequest,
|
||||
new Promise<PluginTerminalProviderResponse>((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
controller.abort();
|
||||
resolve(Object.freeze({ requestId: '', stale: true, results: Object.freeze([]) }));
|
||||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyCurrentProviderResponse(
|
||||
providerResponse: Promise<PluginTerminalProviderResponse>,
|
||||
isCurrent: () => boolean,
|
||||
apply: (response: PluginTerminalProviderResponse) => void,
|
||||
clear: () => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await providerResponse;
|
||||
if (!isCurrent()) return;
|
||||
if (response.stale) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
apply(response);
|
||||
} catch {
|
||||
if (isCurrent()) clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function isPluginTerminalProviderRefreshCurrent(
|
||||
initial: PluginTerminalProviderRefreshMetadata,
|
||||
current: PluginTerminalProviderRefreshMetadata,
|
||||
): boolean {
|
||||
return current.status === 'connected'
|
||||
&& current.sessionId === initial.sessionId
|
||||
&& current.hostId === initial.hostId
|
||||
&& current.workspaceId === initial.workspaceId
|
||||
&& current.protocol === initial.protocol
|
||||
&& current.shellType === initial.shellType
|
||||
&& current.baseTheme === initial.baseTheme;
|
||||
}
|
||||
|
||||
export function usePluginTerminalProviders(options: UsePluginTerminalProvidersOptions) {
|
||||
const registry = getWindowPluginTerminalProviderRegistry();
|
||||
const metadataRef = useRef(options);
|
||||
metadataRef.current = options;
|
||||
const availabilityRef = useRef(new PluginTerminalProviderAvailability());
|
||||
const [providerRevision, setProviderRevision] = useState(0);
|
||||
const [decorationRules, setDecorationRules] = useState<readonly PluginTerminalDecorationRule[]>(Object.freeze([]));
|
||||
const [themeColors, setThemeColors] = useState<PluginTerminalThemeColors>(Object.freeze({}));
|
||||
const refreshGenerationRef = useRef(0);
|
||||
const refreshAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const request = useCallback<RequestPluginTerminalProviders>(async (
|
||||
kind,
|
||||
operation,
|
||||
payload,
|
||||
deadlineMs,
|
||||
supersessionKey,
|
||||
signal,
|
||||
) => {
|
||||
if (!registry) return Object.freeze({ requestId: '', stale: false, results: Object.freeze([]) });
|
||||
const metadata = metadataRef.current;
|
||||
return registry.request({
|
||||
kind,
|
||||
operation,
|
||||
session: {
|
||||
sessionId: metadata.sessionId,
|
||||
...(metadata.hostId ? { hostId: metadata.hostId } : {}),
|
||||
...(metadata.workspaceId ? { workspaceId: metadata.workspaceId } : {}),
|
||||
protocol: metadata.protocol,
|
||||
status: metadata.status,
|
||||
...(metadata.shellType ? { shellType: metadata.shellType } : {}),
|
||||
...metadata.getSnapshotState(),
|
||||
},
|
||||
payload,
|
||||
deadlineMs,
|
||||
supersessionKey,
|
||||
}, { signal });
|
||||
}, [registry]);
|
||||
|
||||
const hasProvider = useCallback(
|
||||
(kind: NetcattyTerminalProviderKind) => availabilityRef.current.has(kind),
|
||||
[],
|
||||
);
|
||||
|
||||
const refreshAvailability = useCallback(async () => {
|
||||
return availabilityRef.current.refresh(registry, ORDINARY_PROVIDER_KINDS);
|
||||
}, [registry]);
|
||||
|
||||
const refreshProviderOutputs = useCallback(async (reason: string) => {
|
||||
const generation = ++refreshGenerationRef.current;
|
||||
refreshAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
refreshAbortRef.current = controller;
|
||||
const metadata = metadataRef.current;
|
||||
if (!registry || metadata.status !== 'connected') {
|
||||
setDecorationRules(Object.freeze([]));
|
||||
setThemeColors(Object.freeze({}));
|
||||
if (refreshAbortRef.current === controller) refreshAbortRef.current = null;
|
||||
return;
|
||||
}
|
||||
const tasks: Promise<void>[] = [];
|
||||
const isCurrent = () => {
|
||||
const current = metadataRef.current;
|
||||
return generation === refreshGenerationRef.current
|
||||
&& isPluginTerminalProviderRefreshCurrent(metadata, current);
|
||||
};
|
||||
if (hasProvider('terminal.decoration')) {
|
||||
tasks.push(applyCurrentProviderResponse(waitForProviderResponse(
|
||||
request('terminal.decoration', 'provideDecorations', { reason }, PROVIDER_DEADLINE_MS, undefined, controller.signal),
|
||||
controller,
|
||||
), isCurrent,
|
||||
(response) => {
|
||||
setDecorationRules(mergePluginDecorationRules(response.results.map((result) => result.status === 'ok'
|
||||
? normalizePluginDecorationResult(result.providerId, result.result)
|
||||
: Object.freeze([]))));
|
||||
},
|
||||
() => setDecorationRules(Object.freeze([]))));
|
||||
} else {
|
||||
setDecorationRules(Object.freeze([]));
|
||||
}
|
||||
if (hasProvider('terminal.theme')) {
|
||||
tasks.push(applyCurrentProviderResponse(waitForProviderResponse(request('terminal.theme', 'provideTheme', {
|
||||
reason,
|
||||
currentTheme: {
|
||||
type: metadata.baseTheme.type,
|
||||
colors: metadata.baseTheme.colors,
|
||||
},
|
||||
}, PROVIDER_DEADLINE_MS, undefined, controller.signal), controller), isCurrent,
|
||||
(response) => {
|
||||
setThemeColors(mergePluginTerminalThemeColors(response.results.map((result) => result.status === 'ok'
|
||||
? normalizePluginThemeResult(result.providerId, result.result)
|
||||
: Object.freeze({}))));
|
||||
},
|
||||
() => setThemeColors(Object.freeze({}))));
|
||||
} else {
|
||||
setThemeColors(Object.freeze({}));
|
||||
}
|
||||
try {
|
||||
await Promise.all(tasks);
|
||||
} finally {
|
||||
if (refreshAbortRef.current === controller) refreshAbortRef.current = null;
|
||||
}
|
||||
}, [hasProvider, registry, request]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const refresh = async () => {
|
||||
if (!await refreshAvailability() || !active) return;
|
||||
setProviderRevision((value) => value + 1);
|
||||
};
|
||||
void refresh();
|
||||
const subscription = registry?.onDidChangeProviders(() => { void refresh(); });
|
||||
return () => {
|
||||
active = false;
|
||||
subscription?.();
|
||||
refreshGenerationRef.current += 1;
|
||||
refreshAbortRef.current?.abort();
|
||||
};
|
||||
}, [refreshAvailability, registry]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshProviderOutputs('session-state');
|
||||
}, [
|
||||
options.status,
|
||||
options.baseTheme,
|
||||
options.sessionId,
|
||||
options.hostId,
|
||||
options.workspaceId,
|
||||
options.protocol,
|
||||
options.shellType,
|
||||
providerRevision,
|
||||
refreshProviderOutputs,
|
||||
]);
|
||||
|
||||
const resolvedTheme = useMemo(
|
||||
() => resolvePluginTerminalTheme(options.baseTheme, themeColors),
|
||||
[options.baseTheme, themeColors],
|
||||
);
|
||||
|
||||
return useMemo(() => ({
|
||||
decorationRules,
|
||||
hasProvider,
|
||||
providerRevision,
|
||||
refreshProviderOutputs,
|
||||
request,
|
||||
resolvedTheme,
|
||||
}), [decorationRules, hasProvider, providerRevision, refreshProviderOutputs, request, resolvedTheme]);
|
||||
}
|
||||
Reference in New Issue
Block a user