import { type GitActionProgressEvent, type GitRunStackedActionInput, type GitRunStackedActionResult, type LocalApi, ORCHESTRATION_WS_METHODS, type ServerSettingsPatch, type VcsStatusResult, type VcsStatusStreamEvent, WS_METHODS, } from "@t3tools/contracts"; import { applyGitStatusStreamEvent } from "@t3tools/shared/git"; import type * as Effect from "effect/Effect"; import type * as Stream from "effect/Stream"; import { type WsRpcProtocolClient } from "./wsRpcProtocol.ts"; import { WsTransport } from "./wsTransport.ts"; type RpcTag = keyof WsRpcProtocolClient & string; type RpcMethod = WsRpcProtocolClient[TTag]; type RpcInput = Parameters>[0]; interface StreamSubscriptionOptions { readonly onResubscribe?: () => void; } function subscriptionOptions( options: StreamSubscriptionOptions | undefined, tag: string, ): StreamSubscriptionOptions & { readonly tag: string } { return { ...options, tag, }; } type RpcUnaryMethod = RpcMethod extends (input: any, options?: any) => Effect.Effect ? (input: RpcInput) => Promise : never; type RpcUnaryNoArgMethod = RpcMethod extends (input: any, options?: any) => Effect.Effect ? () => Promise : never; type RpcStreamMethod = RpcMethod extends (input: any, options?: any) => Stream.Stream ? (listener: (event: TEvent) => void, options?: StreamSubscriptionOptions) => () => void : never; type RpcInputStreamMethod = RpcMethod extends (input: any, options?: any) => Stream.Stream ? ( input: RpcInput, listener: (event: TEvent) => void, options?: StreamSubscriptionOptions, ) => () => void : never; interface GitRunStackedActionOptions { readonly onProgress?: (event: GitActionProgressEvent) => void; } export interface WsRpcClient { readonly dispose: () => Promise; readonly reconnect: () => Promise; readonly isHeartbeatFresh: () => boolean; readonly terminal: { readonly open: RpcUnaryMethod; readonly attach: RpcInputStreamMethod; readonly write: RpcUnaryMethod; readonly resize: RpcUnaryMethod; readonly clear: RpcUnaryMethod; readonly restart: RpcUnaryMethod; readonly close: RpcUnaryMethod; readonly onEvent: RpcStreamMethod; readonly onMetadata: RpcStreamMethod; }; readonly projects: { readonly searchEntries: RpcUnaryMethod; readonly writeFile: RpcUnaryMethod; }; readonly filesystem: { readonly browse: RpcUnaryMethod; }; readonly sourceControl: { readonly lookupRepository: RpcUnaryMethod; readonly cloneRepository: RpcUnaryMethod; readonly publishRepository: RpcUnaryMethod; }; readonly shell: { readonly openInEditor: (input: { readonly cwd: Parameters[0]; readonly editor: Parameters[1]; }) => ReturnType; }; readonly vcs: { readonly pull: RpcUnaryMethod; readonly refreshStatus: RpcUnaryMethod; readonly onStatus: ( input: RpcInput, listener: (status: VcsStatusResult) => void, options?: StreamSubscriptionOptions, ) => () => void; readonly listRefs: RpcUnaryMethod; readonly createWorktree: RpcUnaryMethod; readonly removeWorktree: RpcUnaryMethod; readonly createRef: RpcUnaryMethod; readonly switchRef: RpcUnaryMethod; readonly init: RpcUnaryMethod; }; readonly git: { readonly runStackedAction: ( input: GitRunStackedActionInput, options?: GitRunStackedActionOptions, ) => Promise; readonly resolvePullRequest: RpcUnaryMethod; readonly preparePullRequestThread: RpcUnaryMethod< typeof WS_METHODS.gitPreparePullRequestThread >; }; readonly review: { readonly getDiffPreview: RpcUnaryMethod; }; readonly server: { readonly getConfig: RpcUnaryNoArgMethod; readonly refreshProviders: ( input?: RpcInput, ) => ReturnType>; readonly discoverSourceControl: RpcUnaryNoArgMethod< typeof WS_METHODS.serverDiscoverSourceControl >; readonly updateProvider: RpcUnaryMethod; readonly upsertKeybinding: RpcUnaryMethod; readonly removeKeybinding: RpcUnaryMethod; readonly getSettings: RpcUnaryNoArgMethod; readonly updateSettings: ( patch: ServerSettingsPatch, ) => ReturnType>; readonly subscribeConfig: RpcStreamMethod; readonly subscribeLifecycle: RpcStreamMethod; readonly subscribeAuthAccess: RpcStreamMethod; readonly getTraceDiagnostics: RpcUnaryNoArgMethod; readonly getProcessDiagnostics: RpcUnaryNoArgMethod< typeof WS_METHODS.serverGetProcessDiagnostics >; readonly getProcessResourceHistory: RpcUnaryMethod< typeof WS_METHODS.serverGetProcessResourceHistory >; readonly signalProcess: RpcUnaryMethod; }; readonly orchestration: { readonly dispatchCommand: RpcUnaryMethod; readonly getTurnDiff: RpcUnaryMethod; readonly getFullThreadDiff: RpcUnaryMethod; readonly getArchivedShellSnapshot: RpcUnaryNoArgMethod< typeof ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot >; readonly subscribeShell: RpcStreamMethod; readonly subscribeThread: RpcInputStreamMethod; }; } export interface CreateWsRpcClientOptions { /** Runs immediately before `transport.reconnect()` (e.g. reset reconnect UI/backoff state). */ readonly beforeReconnect?: () => void; } export function createWsRpcClient( transport: WsTransport, options?: CreateWsRpcClientOptions, ): WsRpcClient { return { dispose: () => transport.dispose(), isHeartbeatFresh: () => transport.isHeartbeatFresh(), reconnect: async () => { options?.beforeReconnect?.(); await transport.reconnect(); }, terminal: { open: (input) => transport.request((client) => client[WS_METHODS.terminalOpen](input)), attach: (input, listener, options) => transport.subscribe( (client) => client[WS_METHODS.terminalAttach](input), listener, subscriptionOptions(options, WS_METHODS.terminalAttach), ), write: (input) => transport.request((client) => client[WS_METHODS.terminalWrite](input)), resize: (input) => transport.request((client) => client[WS_METHODS.terminalResize](input)), clear: (input) => transport.request((client) => client[WS_METHODS.terminalClear](input)), restart: (input) => transport.request((client) => client[WS_METHODS.terminalRestart](input)), close: (input) => transport.request((client) => client[WS_METHODS.terminalClose](input)), onEvent: (listener, options) => transport.subscribe( (client) => client[WS_METHODS.subscribeTerminalEvents]({}), listener, subscriptionOptions(options, WS_METHODS.subscribeTerminalEvents), ), onMetadata: (listener, options) => transport.subscribe( (client) => client[WS_METHODS.subscribeTerminalMetadata]({}), listener, subscriptionOptions(options, WS_METHODS.subscribeTerminalMetadata), ), }, projects: { searchEntries: (input) => transport.request((client) => client[WS_METHODS.projectsSearchEntries](input)), writeFile: (input) => transport.request((client) => client[WS_METHODS.projectsWriteFile](input)), }, filesystem: { browse: (input) => transport.request((client) => client[WS_METHODS.filesystemBrowse](input)), }, sourceControl: { lookupRepository: (input) => transport.request((client) => client[WS_METHODS.sourceControlLookupRepository](input)), cloneRepository: (input) => transport.request((client) => client[WS_METHODS.sourceControlCloneRepository](input)), publishRepository: (input) => transport.request((client) => client[WS_METHODS.sourceControlPublishRepository](input)), }, shell: { openInEditor: (input) => transport.request((client) => client[WS_METHODS.shellOpenInEditor](input)), }, vcs: { pull: (input) => transport.request((client) => client[WS_METHODS.vcsPull](input)), refreshStatus: (input) => transport.request((client) => client[WS_METHODS.vcsRefreshStatus](input)), onStatus: (input, listener, options) => { let current: VcsStatusResult | null = null; return transport.subscribe( (client) => client[WS_METHODS.subscribeVcsStatus](input), (event: VcsStatusStreamEvent) => { current = applyGitStatusStreamEvent(current, event); listener(current); }, subscriptionOptions(options, WS_METHODS.subscribeVcsStatus), ); }, listRefs: (input) => transport.request((client) => client[WS_METHODS.vcsListRefs](input)), createWorktree: (input) => transport.request((client) => client[WS_METHODS.vcsCreateWorktree](input)), removeWorktree: (input) => transport.request((client) => client[WS_METHODS.vcsRemoveWorktree](input)), createRef: (input) => transport.request((client) => client[WS_METHODS.vcsCreateRef](input)), switchRef: (input) => transport.request((client) => client[WS_METHODS.vcsSwitchRef](input)), init: (input) => transport.request((client) => client[WS_METHODS.vcsInit](input)), }, git: { runStackedAction: async (input, options) => { let result: GitRunStackedActionResult | null = null; await transport.requestStream( (client) => client[WS_METHODS.gitRunStackedAction](input), (event) => { options?.onProgress?.(event); if (event.kind === "action_finished") { result = event.result; } }, ); if (result) { return result; } throw new Error("Git action stream completed without a final result."); }, resolvePullRequest: (input) => transport.request((client) => client[WS_METHODS.gitResolvePullRequest](input)), preparePullRequestThread: (input) => transport.request((client) => client[WS_METHODS.gitPreparePullRequestThread](input)), }, review: { getDiffPreview: (input) => transport.request((client) => client[WS_METHODS.reviewGetDiffPreview](input)), }, server: { getConfig: () => transport.request((client) => client[WS_METHODS.serverGetConfig]({})), refreshProviders: (input) => transport.request((client) => client[WS_METHODS.serverRefreshProviders](input ?? {})), discoverSourceControl: () => transport.request((client) => client[WS_METHODS.serverDiscoverSourceControl]({})), updateProvider: (input) => transport.request((client) => client[WS_METHODS.serverUpdateProvider](input)), upsertKeybinding: (input) => transport.request((client) => client[WS_METHODS.serverUpsertKeybinding](input)), removeKeybinding: (input) => transport.request((client) => client[WS_METHODS.serverRemoveKeybinding](input)), getSettings: () => transport.request((client) => client[WS_METHODS.serverGetSettings]({})), updateSettings: (patch) => transport.request((client) => client[WS_METHODS.serverUpdateSettings]({ patch })), subscribeConfig: (listener, options) => transport.subscribe( (client) => client[WS_METHODS.subscribeServerConfig]({}), listener, subscriptionOptions(options, WS_METHODS.subscribeServerConfig), ), subscribeLifecycle: (listener, options) => transport.subscribe( (client) => client[WS_METHODS.subscribeServerLifecycle]({}), listener, subscriptionOptions(options, WS_METHODS.subscribeServerLifecycle), ), subscribeAuthAccess: (listener, options) => transport.subscribe( (client) => client[WS_METHODS.subscribeAuthAccess]({}), listener, subscriptionOptions(options, WS_METHODS.subscribeAuthAccess), ), getTraceDiagnostics: () => transport.request((client) => client[WS_METHODS.serverGetTraceDiagnostics]({})), getProcessDiagnostics: () => transport.request((client) => client[WS_METHODS.serverGetProcessDiagnostics]({})), getProcessResourceHistory: (input) => transport.request((client) => client[WS_METHODS.serverGetProcessResourceHistory](input)), signalProcess: (input) => transport.request((client) => client[WS_METHODS.serverSignalProcess](input)), }, orchestration: { dispatchCommand: (input) => transport.request((client) => client[ORCHESTRATION_WS_METHODS.dispatchCommand](input)), getTurnDiff: (input) => transport.request((client) => client[ORCHESTRATION_WS_METHODS.getTurnDiff](input)), getFullThreadDiff: (input) => transport.request((client) => client[ORCHESTRATION_WS_METHODS.getFullThreadDiff](input)), getArchivedShellSnapshot: () => transport.request((client) => client[ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]({}), ), subscribeShell: (listener, options) => transport.subscribe( (client) => client[ORCHESTRATION_WS_METHODS.subscribeShell]({}), listener, subscriptionOptions(options, ORCHESTRATION_WS_METHODS.subscribeShell), ), subscribeThread: (input, listener, options) => transport.subscribe( (client) => client[ORCHESTRATION_WS_METHODS.subscribeThread](input), listener, subscriptionOptions(options, ORCHESTRATION_WS_METHODS.subscribeThread), ), }, }; }