diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 911a601955..1b81963fc7 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -45,6 +45,7 @@ const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49_000; const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19_000; const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000; const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000; +const WORKING_TREE_DIFF_MAX_OUTPUT_BYTES = 512_000; const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; const GIT_CHECK_IGNORE_MAX_STDIN_BYTES = 256 * 1024; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); @@ -1602,6 +1603,46 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { }, ); + const readWorkingTreeDiff: GitCoreShape["readWorkingTreeDiff"] = Effect.fn("readWorkingTreeDiff")( + function* (cwd) { + // Check whether the repository has any commits yet. + const hasCommits = yield* executeGit( + "GitCore.readWorkingTreeDiff.hasCommits", + cwd, + ["rev-parse", "HEAD"], + { allowNonZeroExit: true, timeoutMs: 5_000, maxOutputBytes: 256 }, + ).pipe(Effect.map((result) => result.code === 0)); + + if (hasCommits) { + // Standard case: diff HEAD covers staged + unstaged changes on tracked files. + const trackedDiff = yield* runGitStdoutWithOptions( + "GitCore.readWorkingTreeDiff.trackedDiff", + cwd, + ["diff", "HEAD", "--patch", "--minimal", "--no-color"], + { + maxOutputBytes: WORKING_TREE_DIFF_MAX_OUTPUT_BYTES, + truncateOutputAtMaxBytes: true, + }, + ); + + return trackedDiff; + } + + // No commits yet: show staged changes (initial add). + const stagedDiff = yield* runGitStdoutWithOptions( + "GitCore.readWorkingTreeDiff.stagedDiff", + cwd, + ["diff", "--cached", "--patch", "--minimal", "--no-color"], + { + maxOutputBytes: WORKING_TREE_DIFF_MAX_OUTPUT_BYTES, + truncateOutputAtMaxBytes: true, + }, + ); + + return stagedDiff; + }, + ); + const readConfigValue: GitCoreShape["readConfigValue"] = (cwd, key) => runGitStdout("GitCore.readConfigValue", cwd, ["config", "--get", key], true).pipe( Effect.map((stdout) => stdout.trim()), @@ -2134,6 +2175,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { pushCurrentBranch, pullCurrentBranch, readRangeContext, + readWorkingTreeDiff, readConfigValue, isInsideWorkTree, listWorkspaceFiles, diff --git a/apps/server/src/git/Services/GitCore.ts b/apps/server/src/git/Services/GitCore.ts index 015efa8bbd..32960badb4 100644 --- a/apps/server/src/git/Services/GitCore.ts +++ b/apps/server/src/git/Services/GitCore.ts @@ -197,6 +197,11 @@ export interface GitCoreShape { baseBranch: string, ) => Effect.Effect; + /** + * Read a unified patch of working tree changes (staged + unstaged) on tracked files against HEAD. + */ + readonly readWorkingTreeDiff: (cwd: string) => Effect.Effect; + /** * Read a Git config value from the local repository. */ diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index ca096bff33..9a9271e506 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -660,6 +660,12 @@ const WsRpcLayer = WsRpcGroup.toLayer( git.initRepo(input).pipe(Effect.tap(() => refreshGitStatus(input.cwd))), { "rpc.aggregate": "git" }, ), + [WS_METHODS.gitWorkingTreeDiff]: (input) => + observeRpcEffect( + WS_METHODS.gitWorkingTreeDiff, + git.readWorkingTreeDiff(input.cwd).pipe(Effect.map((diff) => ({ diff }))), + { "rpc.aggregate": "git" }, + ), [WS_METHODS.terminalOpen]: (input) => observeRpcEffect(WS_METHODS.terminalOpen, terminalManager.open(input), { "rpc.aggregate": "terminal", diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index ff216baed7..a2d7b5f306 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -1,12 +1,14 @@ import { parsePatchFiles } from "@pierre/diffs"; import { FileDiff, type FileDiffMetadata, Virtualizer } from "@pierre/diffs/react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { ThreadId, type TurnId } from "@t3tools/contracts"; import { ChevronLeftIcon, ChevronRightIcon, Columns2Icon, + GitBranchIcon, + ListIcon, Rows3Icon, TextWrapIcon, } from "lucide-react"; @@ -21,10 +23,11 @@ import { import { openInPreferredEditor } from "../editorPreferences"; import { useGitStatus } from "~/lib/gitStatusState"; import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; +import { gitQueryKeys, gitWorkingTreeDiffQueryOptions } from "~/lib/gitReactQuery"; import { cn } from "~/lib/utils"; import { readNativeApi } from "../nativeApi"; import { resolvePathLinkTarget } from "../terminal-links"; -import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; +import { type DiffScope, parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { useTheme } from "../hooks/useTheme"; import { buildPatchCacheKey } from "../lib/diffRendering"; import { resolveDiffThemeName } from "../lib/diffRendering"; @@ -180,6 +183,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { }); const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) }); const diffOpen = diffSearch.diff === "1"; + const diffScope: DiffScope = diffSearch.diffScope ?? "session"; const activeThreadId = routeThreadId; const activeThread = useStore((store) => activeThreadId ? store.threads.find((thread) => thread.id === activeThreadId) : undefined, @@ -189,8 +193,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { activeProjectId ? store.projects.find((project) => project.id === activeProjectId) : undefined, ); const activeCwd = activeThread?.worktreePath ?? activeProject?.cwd; + const queryClient = useQueryClient(); const gitStatusQuery = useGitStatus(activeCwd ?? null); const isGitRepo = gitStatusQuery.data?.isRepo ?? true; + + // Invalidate the git working tree diff query whenever git status changes, + // so the diff panel stays in sync without polling. + const gitStatusData = gitStatusQuery.data; + useEffect(() => { + if (!activeCwd || !gitStatusData) return; + void queryClient.invalidateQueries({ queryKey: gitQueryKeys.workingTreeDiff(activeCwd) }); + }, [queryClient, activeCwd, gitStatusData]); const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = useTurnDiffSummaries(activeThread); const orderedTurnDiffSummaries = useMemo( @@ -208,7 +221,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { [inferredCheckpointTurnCountByTurnId, turnDiffSummaries], ); - const selectedTurnId = diffSearch.diffTurnId ?? null; + const selectedTurnId = diffScope === "session" ? (diffSearch.diffTurnId ?? null) : null; const selectedFilePath = selectedTurnId !== null ? (diffSearch.diffFilePath ?? null) : null; const selectedTurn = selectedTurnId === null @@ -260,35 +273,52 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { } return `conversation:${orderedTurnDiffSummaries.map((summary) => summary.turnId).join(",")}`; }, [orderedTurnDiffSummaries, selectedTurn]); + + // --- Session (checkpoint) diff query --- const activeCheckpointDiffQuery = useQuery( checkpointDiffQueryOptions({ threadId: activeThreadId, fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, toTurnCount: activeCheckpointRange?.toTurnCount ?? null, cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, - enabled: isGitRepo, + enabled: isGitRepo && diffScope === "session", }), ); - const selectedTurnCheckpointDiff = selectedTurn + + // --- Git working tree diff query --- + const gitDiffQuery = useQuery( + gitWorkingTreeDiffQueryOptions({ + cwd: activeCwd ?? null, + enabled: isGitRepo && diffScope === "git", + }), + ); + + // --- Derive active patch based on scope --- + const sessionPatch = selectedTurn ? activeCheckpointDiffQuery.data?.diff - : undefined; - const conversationCheckpointDiff = selectedTurn - ? undefined : activeCheckpointDiffQuery.data?.diff; - const isLoadingCheckpointDiff = activeCheckpointDiffQuery.isLoading; - const checkpointDiffError = - activeCheckpointDiffQuery.error instanceof Error - ? activeCheckpointDiffQuery.error.message - : activeCheckpointDiffQuery.error - ? "Failed to load checkpoint diff." - : null; - - const selectedPatch = selectedTurn ? selectedTurnCheckpointDiff : conversationCheckpointDiff; + const gitPatch = gitDiffQuery.data?.diff; + const selectedPatch = diffScope === "git" ? gitPatch : sessionPatch; + const isLoadingPatch = + diffScope === "git" ? gitDiffQuery.isLoading : activeCheckpointDiffQuery.isLoading; + const patchError = + diffScope === "git" + ? gitDiffQuery.error instanceof Error + ? gitDiffQuery.error.message + : gitDiffQuery.error + ? "Failed to load git diff." + : null + : activeCheckpointDiffQuery.error instanceof Error + ? activeCheckpointDiffQuery.error.message + : activeCheckpointDiffQuery.error + ? "Failed to load checkpoint diff." + : null; + const hasResolvedPatch = typeof selectedPatch === "string"; const hasNoNetChanges = hasResolvedPatch && selectedPatch.trim().length === 0; const renderablePatch = useMemo( - () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}`), - [resolvedTheme, selectedPatch], + () => getRenderablePatch(selectedPatch, `diff-panel:${resolvedTheme}:${diffScope}`), + [resolvedTheme, selectedPatch, diffScope], ); const renderableFiles = useMemo(() => { if (!renderablePatch || renderablePatch.kind !== "files") { @@ -331,6 +361,21 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { [activeCwd], ); + const setDiffScope = useCallback( + (scope: DiffScope) => { + if (!activeThread) return; + void navigate({ + to: "/$threadId", + params: { threadId: activeThread.id }, + search: (previous) => { + const rest = stripDiffSearchParams(previous); + return { ...rest, diff: "1", diffScope: scope }; + }, + }); + }, + [activeThread, navigate], + ); + const selectTurn = (turnId: TurnId) => { if (!activeThread) return; void navigate({ @@ -338,7 +383,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { params: { threadId: activeThread.id }, search: (previous) => { const rest = stripDiffSearchParams(previous); - return { ...rest, diff: "1", diffTurnId: turnId }; + return { ...rest, diff: "1", diffTurnId: turnId, diffScope: "session" }; }, }); }; @@ -349,7 +394,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { params: { threadId: activeThread.id }, search: (previous) => { const rest = stripDiffSearchParams(previous); - return { ...rest, diff: "1" }; + return { ...rest, diff: "1", diffScope: "session" }; }, }); }; @@ -417,96 +462,126 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { const headerRow = ( <>
- {canScrollTurnStripLeft && ( -
- )} - {canScrollTurnStripRight && ( -
- )} - - -
-
- - {orderedTurnDiffSummaries.map((summary) => ( + + +
+ - ))} -
+ + {orderedTurnDiffSummaries.map((summary) => ( + + ))} +
+ + )} + {diffScope === "git" && ( +
+ + Working tree changes + +
+ )}
+ { + const next = value[0]; + if (next === "session" || next === "git") { + setDiffScope(next); + } + }} + > + + + + + + + ); + const emptyStateMessage = + diffScope === "git" ? "No working tree changes." : "No completed turns yet."; + const loadingLabel = diffScope === "git" ? "Loading git diff..." : "Loading checkpoint diff..."; + const noChangesLabel = + diffScope === "git" ? "No working tree changes." : "No net changes in this selection."; + const noPatchLabel = + diffScope === "git" ? "No working tree changes." : "No patch available for this selection."; + const showEmptySessionState = diffScope === "session" && orderedTurnDiffSummaries.length === 0; + return ( {!activeThread ? (
- Select a thread to inspect turn diffs. + Select a thread to inspect diffs.
) : !isGitRepo ? (
- Turn diffs are unavailable because this project is not a git repository. + Diffs are unavailable because this project is not a git repository.
- ) : orderedTurnDiffSummaries.length === 0 ? ( + ) : showEmptySessionState ? (
- No completed turns yet. + {emptyStateMessage}
) : ( <> @@ -562,21 +646,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { ref={patchViewportRef} className="diff-panel-viewport min-h-0 min-w-0 flex-1 overflow-hidden" > - {checkpointDiffError && !renderablePatch && ( + {patchError && !renderablePatch && (
-

{checkpointDiffError}

+

{patchError}

)} {!renderablePatch ? ( - isLoadingCheckpointDiff ? ( - + isLoadingPatch ? ( + ) : (
-

- {hasNoNetChanges - ? "No net changes in this selection." - : "No patch available for this selection."} -

+

{hasNoNetChanges ? noChangesLabel : noPatchLabel}

) ) : renderablePatch.kind === "files" ? ( diff --git a/apps/web/src/diffRouteSearch.test.ts b/apps/web/src/diffRouteSearch.test.ts index ef00874bd2..1d2efab878 100644 --- a/apps/web/src/diffRouteSearch.test.ts +++ b/apps/web/src/diffRouteSearch.test.ts @@ -71,4 +71,46 @@ describe("parseDiffRouteSearch", () => { diff: "1", }); }); + + it("parses diffScope when diff is open", () => { + expect( + parseDiffRouteSearch({ + diff: "1", + diffScope: "git", + }), + ).toEqual({ + diff: "1", + diffScope: "git", + }); + + expect( + parseDiffRouteSearch({ + diff: "1", + diffScope: "session", + }), + ).toEqual({ + diff: "1", + diffScope: "session", + }); + }); + + it("drops diffScope when diff is closed", () => { + expect( + parseDiffRouteSearch({ + diff: "0", + diffScope: "git", + }), + ).toEqual({}); + }); + + it("drops invalid diffScope values", () => { + expect( + parseDiffRouteSearch({ + diff: "1", + diffScope: "invalid", + }), + ).toEqual({ + diff: "1", + }); + }); }); diff --git a/apps/web/src/diffRouteSearch.ts b/apps/web/src/diffRouteSearch.ts index d7de23e348..1acd7ac44d 100644 --- a/apps/web/src/diffRouteSearch.ts +++ b/apps/web/src/diffRouteSearch.ts @@ -1,9 +1,12 @@ import { TurnId } from "@t3tools/contracts"; +export type DiffScope = "session" | "git"; + export interface DiffRouteSearch { diff?: "1" | undefined; diffTurnId?: TurnId | undefined; diffFilePath?: string | undefined; + diffScope?: DiffScope | undefined; } function isDiffOpenValue(value: unknown): boolean { @@ -18,11 +21,21 @@ function normalizeSearchString(value: unknown): string | undefined { return normalized.length > 0 ? normalized : undefined; } +function isDiffScope(value: unknown): value is DiffScope { + return value === "session" || value === "git"; +} + export function stripDiffSearchParams>( params: T, -): Omit { - const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = params; - return rest as Omit; +): Omit { + const { + diff: _diff, + diffTurnId: _diffTurnId, + diffFilePath: _diffFilePath, + diffScope: _diffScope, + ...rest + } = params; + return rest as Omit; } export function parseDiffRouteSearch(search: Record): DiffRouteSearch { @@ -30,10 +43,12 @@ export function parseDiffRouteSearch(search: Record): DiffRoute const diffTurnIdRaw = diff ? normalizeSearchString(search.diffTurnId) : undefined; const diffTurnId = diffTurnIdRaw ? TurnId.makeUnsafe(diffTurnIdRaw) : undefined; const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined; + const diffScope = diff && isDiffScope(search.diffScope) ? search.diffScope : undefined; return { ...(diff ? { diff } : {}), ...(diffTurnId ? { diffTurnId } : {}), ...(diffFilePath ? { diffFilePath } : {}), + ...(diffScope ? { diffScope } : {}), }; } diff --git a/apps/web/src/lib/gitReactQuery.ts b/apps/web/src/lib/gitReactQuery.ts index a2611ebe25..5006a580a2 100644 --- a/apps/web/src/lib/gitReactQuery.ts +++ b/apps/web/src/lib/gitReactQuery.ts @@ -21,6 +21,7 @@ export const gitQueryKeys = { branches: (cwd: string | null) => ["git", "branches", cwd] as const, branchSearch: (cwd: string | null, query: string) => ["git", "branches", cwd, "search", query] as const, + workingTreeDiff: (cwd: string | null) => ["git", "workingTreeDiff", cwd] as const, }; export const gitMutationKeys = { @@ -207,6 +208,20 @@ export function gitRemoveWorktreeMutationOptions(input: { queryClient: QueryClie }); } +export function gitWorkingTreeDiffQueryOptions(input: { cwd: string | null; enabled?: boolean }) { + return queryOptions({ + queryKey: gitQueryKeys.workingTreeDiff(input.cwd), + queryFn: async () => { + const api = ensureNativeApi(); + if (!input.cwd) throw new Error("Git working tree diff is unavailable."); + return api.git.workingTreeDiff({ cwd: input.cwd }); + }, + enabled: input.cwd !== null && (input.enabled ?? true), + staleTime: 0, + refetchOnWindowFocus: true, + }); +} + export function gitPreparePullRequestThreadMutationOptions(input: { cwd: string | null; queryClient: QueryClient; diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index 3cfb976e09..2785aaaf3b 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -77,6 +77,7 @@ export function createWsNativeApi(): NativeApi { createBranch: rpcClient.git.createBranch, checkout: rpcClient.git.checkout, init: rpcClient.git.init, + workingTreeDiff: rpcClient.git.workingTreeDiff, resolvePullRequest: rpcClient.git.resolvePullRequest, preparePullRequestThread: rpcClient.git.preparePullRequestThread, }, diff --git a/apps/web/src/wsRpcClient.ts b/apps/web/src/wsRpcClient.ts index 997b83d2d7..21cd59f809 100644 --- a/apps/web/src/wsRpcClient.ts +++ b/apps/web/src/wsRpcClient.ts @@ -83,6 +83,7 @@ export interface WsRpcClient { readonly createBranch: RpcUnaryMethod; readonly checkout: RpcUnaryMethod; readonly init: RpcUnaryMethod; + readonly workingTreeDiff: RpcUnaryMethod; readonly resolvePullRequest: RpcUnaryMethod; readonly preparePullRequestThread: RpcUnaryMethod< typeof WS_METHODS.gitPreparePullRequestThread @@ -199,6 +200,8 @@ export function createWsRpcClient(transport = new WsTransport()): WsRpcClient { transport.request((client) => client[WS_METHODS.gitCreateBranch](input)), checkout: (input) => transport.request((client) => client[WS_METHODS.gitCheckout](input)), init: (input) => transport.request((client) => client[WS_METHODS.gitInit](input)), + workingTreeDiff: (input) => + transport.request((client) => client[WS_METHODS.gitWorkingTreeDiff](input)), resolvePullRequest: (input) => transport.request((client) => client[WS_METHODS.gitResolvePullRequest](input)), preparePullRequestThread: (input) => diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 345208acf9..73608f8e79 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -105,6 +105,16 @@ export type GitResolvedPullRequest = typeof GitResolvedPullRequest.Type; // RPC Inputs +export const GitWorkingTreeDiffInput = Schema.Struct({ + cwd: TrimmedNonEmptyStringSchema, +}); +export type GitWorkingTreeDiffInput = typeof GitWorkingTreeDiffInput.Type; + +export const GitWorkingTreeDiffResult = Schema.Struct({ + diff: Schema.String, +}); +export type GitWorkingTreeDiffResult = typeof GitWorkingTreeDiffResult.Type; + export const GitStatusInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, }); diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 630ccd8249..8e9cfc15de 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -17,6 +17,8 @@ import type { GitStatusInput, GitStatusResult, GitCreateBranchResult, + GitWorkingTreeDiffInput, + GitWorkingTreeDiffResult, } from "./git"; import type { ProjectSearchEntriesInput, @@ -157,6 +159,8 @@ export interface NativeApi { preparePullRequestThread: ( input: GitPreparePullRequestThreadInput, ) => Promise; + // Working tree diff + workingTreeDiff: (input: GitWorkingTreeDiffInput) => Promise; // Stacked action API pull: (input: GitPullInput) => Promise; refreshStatus: (input: GitStatusInput) => Promise; diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index a3d10299df..331d5bafc5 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -27,6 +27,8 @@ import { GitStatusInput, GitStatusResult, GitStatusStreamEvent, + GitWorkingTreeDiffInput, + GitWorkingTreeDiffResult, } from "./git"; import { KeybindingsConfigError } from "./keybindings"; import { @@ -94,6 +96,7 @@ export const WS_METHODS = { gitCreateBranch: "git.createBranch", gitCheckout: "git.checkout", gitInit: "git.init", + gitWorkingTreeDiff: "git.workingTreeDiff", gitResolvePullRequest: "git.resolvePullRequest", gitPreparePullRequestThread: "git.preparePullRequestThread", @@ -238,6 +241,12 @@ export const WsGitInitRpc = Rpc.make(WS_METHODS.gitInit, { error: GitCommandError, }); +export const WsGitWorkingTreeDiffRpc = Rpc.make(WS_METHODS.gitWorkingTreeDiff, { + payload: GitWorkingTreeDiffInput, + success: GitWorkingTreeDiffResult, + error: GitCommandError, +}); + export const WsTerminalOpenRpc = Rpc.make(WS_METHODS.terminalOpen, { payload: TerminalOpenInput, success: TerminalSessionSnapshot, @@ -355,6 +364,7 @@ export const WsRpcGroup = RpcGroup.make( WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, + WsGitWorkingTreeDiffRpc, WsTerminalOpenRpc, WsTerminalWriteRpc, WsTerminalResizeRpc,