[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:
143
components/terminal/autocomplete/terminalCompletionProviders.ts
Normal file
143
components/terminal/autocomplete/terminalCompletionProviders.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
mergePluginCompletionItems,
|
||||
normalizePluginCompletionResult,
|
||||
} from '../../../domain/pluginTerminalProviders';
|
||||
import type { PluginTerminalProviderRegistry } from '../../../application/state/pluginTerminalProviderRegistry';
|
||||
import {
|
||||
getCompletions,
|
||||
type CompletionSuggestion,
|
||||
} from './completionEngine';
|
||||
import type { AutocompleteCwdSource } from './terminalAutocompleteLayout';
|
||||
import type { AutocompleteHistoryScope, Snippet } from '../../../domain/models';
|
||||
|
||||
export interface TerminalCompletionProviderRequest {
|
||||
input: string;
|
||||
session: NetcattyTerminalSessionSnapshot;
|
||||
hostGroup?: string;
|
||||
hostOs: 'linux' | 'windows' | 'macos';
|
||||
cwdSource?: AutocompleteCwdSource;
|
||||
snippets?: Snippet[];
|
||||
maximum: number;
|
||||
/** Which history pool built-in suggestions draw from. */
|
||||
historyScope?: AutocompleteHistoryScope;
|
||||
/** Internal end-to-end wait bound; tests may lower it deterministically. */
|
||||
pluginResponseTimeoutMs?: number;
|
||||
/** Host security/session cancellation propagated to the plugin bridge. */
|
||||
signal?: AbortSignal;
|
||||
/**
|
||||
* Forwarded to built-in getCompletions when a path listing finishes after the
|
||||
* soft budget (cache-bypassed relative SSH cwd).
|
||||
*/
|
||||
onLatePathSuggestions?: (suggestions: CompletionSuggestion[]) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PLUGIN_COMPLETION_RESPONSE_TIMEOUT_MS = 800;
|
||||
|
||||
type PluginCompletionResponse = Awaited<ReturnType<PluginTerminalProviderRegistry['request']>>;
|
||||
|
||||
function emptyPluginCompletionResponse(): PluginCompletionResponse {
|
||||
return { requestId: '', stale: false, results: Object.freeze([]) };
|
||||
}
|
||||
|
||||
async function waitForPluginCompletionResponse(
|
||||
response: Promise<PluginCompletionResponse>,
|
||||
timeoutMs: number,
|
||||
onTimeout?: () => void,
|
||||
): Promise<PluginCompletionResponse> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<PluginCompletionResponse>((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
onTimeout?.();
|
||||
resolve(emptyPluginCompletionResponse());
|
||||
}, timeoutMs);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([response, timeout]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
export async function provideTerminalCompletions(
|
||||
registry: PluginTerminalProviderRegistry | null,
|
||||
request: TerminalCompletionProviderRequest,
|
||||
): Promise<CompletionSuggestion[]> {
|
||||
const builtInPromise = getCompletions(request.input, {
|
||||
hostId: request.session.hostId,
|
||||
hostGroup: request.hostGroup,
|
||||
os: request.hostOs,
|
||||
maxResults: request.maximum,
|
||||
sessionId: request.session.sessionId,
|
||||
protocol: request.session.protocol,
|
||||
cwd: request.session.cwd,
|
||||
cwdSource: request.cwdSource,
|
||||
snippets: request.snippets,
|
||||
historyScope: request.historyScope,
|
||||
onLatePathSuggestions: request.onLatePathSuggestions,
|
||||
});
|
||||
const pluginRequestController = new AbortController();
|
||||
const abortPluginRequest = () => pluginRequestController.abort();
|
||||
request.signal?.addEventListener('abort', abortPluginRequest, { once: true });
|
||||
if (request.signal?.aborted) pluginRequestController.abort();
|
||||
const pluginPromise = registry?.request({
|
||||
kind: 'terminal.completion',
|
||||
operation: 'provideCompletions',
|
||||
session: request.session,
|
||||
payload: {
|
||||
input: request.input,
|
||||
cursor: request.input.length,
|
||||
hostOs: request.hostOs,
|
||||
cwdSource: request.cwdSource ?? null,
|
||||
maximum: request.maximum,
|
||||
},
|
||||
deadlineMs: 750,
|
||||
}, { signal: pluginRequestController.signal }).catch(() => emptyPluginCompletionResponse())
|
||||
?? Promise.resolve(emptyPluginCompletionResponse());
|
||||
const pluginResponseTimeoutMs = Number.isFinite(request.pluginResponseTimeoutMs)
|
||||
? Math.max(1, Math.min(5_000, Math.trunc(request.pluginResponseTimeoutMs ?? 0)))
|
||||
: DEFAULT_PLUGIN_COMPLETION_RESPONSE_TIMEOUT_MS;
|
||||
let builtIn: CompletionSuggestion[];
|
||||
let pluginResponse: PluginCompletionResponse;
|
||||
try {
|
||||
[builtIn, pluginResponse] = await Promise.all([
|
||||
builtInPromise,
|
||||
waitForPluginCompletionResponse(
|
||||
pluginPromise,
|
||||
pluginResponseTimeoutMs,
|
||||
() => pluginRequestController.abort(),
|
||||
),
|
||||
]);
|
||||
} finally {
|
||||
request.signal?.removeEventListener('abort', abortPluginRequest);
|
||||
}
|
||||
if (request.signal?.aborted || pluginRequestController.signal.aborted || pluginResponse.stale) {
|
||||
return builtIn;
|
||||
}
|
||||
const pluginGroups = pluginResponse.results.map((result) => result.status === 'ok'
|
||||
? normalizePluginCompletionResult(result.providerId, result.result)
|
||||
: Object.freeze([]));
|
||||
const pluginItems = mergePluginCompletionItems(pluginGroups, request.maximum);
|
||||
const combined: CompletionSuggestion[] = [
|
||||
...builtIn,
|
||||
...pluginItems.map((item) => ({
|
||||
text: item.text,
|
||||
displayText: item.displayText,
|
||||
...(item.description === undefined ? {} : { description: item.description }),
|
||||
source: 'plugin' as const,
|
||||
score: item.score,
|
||||
providerId: item.providerId,
|
||||
})),
|
||||
];
|
||||
const deduplicated = new Map<string, CompletionSuggestion>();
|
||||
for (const item of combined) {
|
||||
const existing = deduplicated.get(item.text);
|
||||
if (!existing
|
||||
|| (existing.source === 'plugin' && item.source !== 'plugin')
|
||||
|| (existing.source === item.source && item.score > existing.score)) {
|
||||
deduplicated.set(item.text, item);
|
||||
}
|
||||
}
|
||||
return [...deduplicated.values()]
|
||||
.sort((left, right) => right.score - left.score || left.text.localeCompare(right.text))
|
||||
.slice(0, request.maximum);
|
||||
}
|
||||
Reference in New Issue
Block a user