Inline chat server request adapters

This commit is contained in:
murashit 2026-06-22 13:55:12 +09:00
parent 1e3e4c9f84
commit 1b95c91034
11 changed files with 66 additions and 76 deletions

View file

@ -94,10 +94,10 @@ const chatAppServerProtocolBoundaryBannedModules = nonAppServerBannedAppServerPr
const chatAppServerProtocolBoundaryBannedImportPatterns = chatAppServerProtocolBoundaryBannedModules.flatMap((moduleName) =>
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
);
const chatAppServerRequestBridgeBannedModules = nonAppServerBannedAppServerProtocolModules.filter(
const chatAppServerRequestBoundaryBannedModules = nonAppServerBannedAppServerProtocolModules.filter(
(moduleName) => moduleName !== "server-requests",
);
const chatAppServerRequestBridgeBannedImportPatterns = chatAppServerRequestBridgeBannedModules.flatMap((moduleName) =>
const chatAppServerRequestBoundaryBannedImportPatterns = chatAppServerRequestBoundaryBannedModules.flatMap((moduleName) =>
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
);
const generatedAppServerThreadImportRestrictions = [
@ -124,7 +124,10 @@ const chatAppServerProtocolBoundaryFiles = [
"src/features/chat/app-server/inbound/notification-plan.ts",
"src/features/chat/app-server/mappers/message-stream/turn-items.ts",
];
const chatAppServerRequestBridgeFiles = ["src/features/chat/app-server/requests/**/*.{ts,tsx}"];
const chatAppServerRequestBoundaryFiles = [
"src/features/chat/app-server/inbound/handler.ts",
"src/features/chat/app-server/inbound/routing.ts",
];
const unsafeIteratorRestrictions = [
{
selector: "MemberExpression[property.name='value'][object.type='CallExpression'][object.callee.property.name='next']",
@ -501,16 +504,16 @@ export default defineConfig([
},
},
{
files: chatAppServerRequestBridgeFiles,
files: chatAppServerRequestBoundaryFiles,
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: chatAppServerRequestBridgeBannedImportPatterns,
group: chatAppServerRequestBoundaryBannedImportPatterns,
message:
"Chat request bridges may consume server request protocol projections only. Convert app-server payloads to chat pending request domain models at this boundary.",
"Chat app-server request handling may consume server request protocol projections only. Convert app-server payloads to chat pending request domain models at this boundary.",
},
{
group: generatedAppServerSourceImportPatterns,

View file

@ -1,4 +1,9 @@
import type { RequestId, ServerNotification, ServerRequest } from "../../../../app-server/connection/rpc-messages";
import {
appServerApprovalResponse,
appServerMcpElicitationResponse,
appServerUserInputResponse,
} from "../../../../app-server/protocol/server-requests";
import type { McpServerStartupStatus } from "../../../../domain/server/diagnostics";
import type { Thread } from "../../../../domain/threads/model";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
@ -17,9 +22,6 @@ import {
type PendingRequestId,
type PendingUserInput,
} from "../../../../domain/pending-requests/model";
import { approvalResponse } from "../requests/approval";
import { mcpElicitationResponse } from "../requests/mcp-elicitation";
import { userInputResponse } from "../requests/user-input";
import {
createApprovalResultItem,
createMcpElicitationResultItem,
@ -177,7 +179,7 @@ function handleAppServerLog(context: ChatInboundHandlerContext, message: string)
function resolveApproval(context: ChatInboundHandlerContext, requestId: PendingRequestId, action: ApprovalAction): void {
const approval = pendingApproval(context, requestId);
if (!approval) return;
if (!context.actions.respondToServerRequest(approval.requestId, approvalResponse(approval, action))) {
if (!context.actions.respondToServerRequest(approval.requestId, appServerApprovalResponse(approval, action))) {
addSystemMessage(context, cannotSendApprovalResponseMessage());
return;
}
@ -187,7 +189,7 @@ function resolveApproval(context: ChatInboundHandlerContext, requestId: PendingR
function resolveUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId, answers: Record<string, string>): void {
const input = pendingUserInput(context, requestId);
if (!input) return;
if (!context.actions.respondToServerRequest(input.requestId, userInputResponse(input, answers))) {
if (!context.actions.respondToServerRequest(input.requestId, appServerUserInputResponse(input.params.questions, answers))) {
addSystemMessage(context, cannotSendUserInputMessage());
return;
}
@ -216,7 +218,7 @@ function resolveMcpElicitation(context: ChatInboundHandlerContext, requestId: Pe
const elicitation = pendingMcpElicitation(context, requestId);
if (!elicitation) return;
const content = action === "accept" ? contentForPendingMcpElicitation(elicitation, state(context).requests.mcpElicitationDrafts) : null;
if (!context.actions.respondToServerRequest(elicitation.requestId, mcpElicitationResponse(action, content))) {
if (!context.actions.respondToServerRequest(elicitation.requestId, appServerMcpElicitationResponse(action, content))) {
addSystemMessage(context, cannotSendMcpElicitationMessage());
return;
}

View file

@ -1,8 +1,10 @@
import type { ServerNotification, ServerRequest } from "../../../../app-server/connection/rpc-messages";
import {
appServerApprovalRequest,
appServerMcpElicitationRequest,
appServerUserInputRequest,
} from "../../../../app-server/protocol/server-requests";
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../domain/pending-requests/model";
import { toPendingApproval } from "../requests/approval";
import { toPendingMcpElicitation } from "../requests/mcp-elicitation";
import { toPendingUserInput } from "../requests/user-input";
export interface ActiveRouteScope {
activeThreadId: string | null;
@ -234,17 +236,17 @@ export function routeServerRequest(request: ServerRequest, scope: ActiveRouteSco
switch (SERVER_REQUEST_ROUTE_KIND_BY_METHOD[request.method]) {
case "approval": {
const approval = toPendingApproval(request);
const approval = appServerApprovalRequest(request);
if (approval) return { kind: "approval", request, approval };
return { kind: "unsupported", request };
}
case "userInput": {
const input = toPendingUserInput(request);
const input = appServerUserInputRequest(request);
if (input) return { kind: "userInput", request, input };
return { kind: "unsupported", request };
}
case "mcpElicitation": {
const elicitation = toPendingMcpElicitation(request);
const elicitation = appServerMcpElicitationRequest(request);
if (elicitation) return { kind: "mcpElicitation", request, elicitation };
return { kind: "unsupported", request };
}

View file

@ -1,15 +0,0 @@
import type { ServerRequest } from "../../../../app-server/connection/rpc-messages";
import {
appServerApprovalRequest,
appServerApprovalResponse,
type AppServerApprovalResponse,
} from "../../../../app-server/protocol/server-requests";
import type { ApprovalAction, PendingApproval } from "../../../../domain/pending-requests/model";
export function toPendingApproval(request: ServerRequest): PendingApproval | null {
return appServerApprovalRequest(request);
}
export function approvalResponse(approval: PendingApproval, action: ApprovalAction): AppServerApprovalResponse {
return appServerApprovalResponse(approval, action);
}

View file

@ -1,18 +0,0 @@
import type { ServerRequest } from "../../../../app-server/connection/rpc-messages";
import {
appServerMcpElicitationRequest,
appServerMcpElicitationResponse,
type AppServerMcpElicitationResponse,
} from "../../../../app-server/protocol/server-requests";
import type { McpElicitationAction, McpElicitationContentValue, PendingMcpElicitation } from "../../../../domain/pending-requests/model";
export function toPendingMcpElicitation(request: ServerRequest): PendingMcpElicitation | null {
return appServerMcpElicitationRequest(request);
}
export function mcpElicitationResponse(
action: McpElicitationAction,
content: Record<string, McpElicitationContentValue> | null,
): AppServerMcpElicitationResponse {
return appServerMcpElicitationResponse(action, content);
}

View file

@ -1,15 +0,0 @@
import type { ServerRequest } from "../../../../app-server/connection/rpc-messages";
import {
appServerUserInputRequest,
appServerUserInputResponse,
type AppServerUserInputResponse,
} from "../../../../app-server/protocol/server-requests";
import type { PendingUserInput } from "../../../../domain/pending-requests/model";
export function toPendingUserInput(request: ServerRequest): PendingUserInput | null {
return appServerUserInputRequest(request);
}
export function userInputResponse(input: PendingUserInput, answers: Record<string, string>): AppServerUserInputResponse {
return appServerUserInputResponse(input.params.questions, answers);
}

View file

@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { appServerUserInputRequest as toPendingUserInput } from "../../../../../src/app-server/protocol/server-requests";
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
import { createPendingRequestActions } from "../../../../../src/features/chat/application/pending-requests/pending-request-actions";
import { toPendingUserInput } from "../../../../../src/features/chat/app-server/requests/user-input";
import type { ServerRequest } from "../../../../../src/app-server/connection/rpc-messages";
function expectPresent<T>(value: T | null | undefined): T {

View file

@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { approvalResponse, toPendingApproval } from "../../../../../src/features/chat/app-server/requests/approval";
import {
appServerApprovalRequest as toPendingApproval,
appServerApprovalResponse as approvalResponse,
} from "../../../../../src/app-server/protocol/server-requests";
import { approvalDetails, approvalSummary, approvalTitle } from "../../../../../src/features/chat/domain/pending-requests/approval";
import { createApprovalResultItem } from "../../../../../src/features/chat/domain/pending-requests/result-items";
import type { CommandApprovalDecision } from "../../../../../src/domain/pending-requests/model";

View file

@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import {
appServerMcpElicitationRequest as toPendingMcpElicitation,
appServerMcpElicitationResponse as mcpElicitationResponse,
} from "../../../../../src/app-server/protocol/server-requests";
import type { ServerRequest } from "../../../../../src/app-server/connection/rpc-messages";
import { toPendingMcpElicitation, mcpElicitationResponse } from "../../../../../src/features/chat/app-server/requests/mcp-elicitation";
import { contentForPendingMcpElicitation, mcpElicitationDraftKey } from "../../../../../src/domain/pending-requests/model";
function expectPresent<T>(value: T | null | undefined): T {

View file

@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import {
appServerUserInputRequest as toPendingUserInput,
appServerUserInputResponse,
} from "../../../../../src/app-server/protocol/server-requests";
import { answersForPendingUserInput, questionDefaultAnswer } from "../../../../../src/domain/pending-requests/model";
import { toPendingUserInput, userInputResponse } from "../../../../../src/features/chat/app-server/requests/user-input";
import {
pendingRequestFocusSignature,
pendingRequestsSignature,
@ -41,7 +44,7 @@ describe("user input model", () => {
expect(questionDefaultAnswer(expectPresent(request.params.questions[0]))).toBe("Recommended");
expect(answersForPendingUserInput(input, new Map())).toEqual({ direction: "Recommended" });
expect(answersForPendingUserInput(input, new Map([["7:direction", "Left"]]))).toEqual({ direction: "Left" });
expect(userInputResponse(input, { direction: "Recommended" })).toEqual({
expect(appServerUserInputResponse(input.params.questions, { direction: "Recommended" })).toEqual({
answers: { direction: { answers: ["Recommended"] } },
});
});

View file

@ -162,7 +162,7 @@ export type Change = FileUpdateChange;
expect(messages).toContain("no-restricted-imports");
});
it("reports server request protocol imports outside app-server and chat request bridges", async () => {
it("reports server request protocol imports outside app-server and chat request boundaries", async () => {
const messages = await lintSource(
"src/features/chat/panel/surface/message-stream-presenter.ts",
`
@ -175,9 +175,17 @@ export const response = appServerUserInputResponse;
expect(messages).toContain("no-restricted-imports");
});
it("allows server request protocol imports in chat request bridges", async () => {
const messages = await lintSource(
"src/features/chat/app-server/requests/user-input.ts",
it("allows server request protocol imports in chat inbound request boundaries", async () => {
const routingMessages = await lintSource(
"src/features/chat/app-server/inbound/routing.ts",
`
import { appServerUserInputResponse } from "../../../../app-server/protocol/server-requests";
export const response = appServerUserInputResponse;
`,
);
const handlerMessages = await lintSource(
"src/features/chat/app-server/inbound/handler.ts",
`
import { appServerUserInputResponse } from "../../../../app-server/protocol/server-requests";
@ -185,7 +193,21 @@ export const response = appServerUserInputResponse;
`,
);
expect(messages).not.toContain("no-restricted-imports");
expect(routingMessages).not.toContain("no-restricted-imports");
expect(handlerMessages).not.toContain("no-restricted-imports");
});
it("reports server request protocol imports in non-boundary chat app-server modules", async () => {
const messages = await lintSource(
"src/features/chat/app-server/inbound/app-server-logs.ts",
`
import { appServerUserInputResponse } from "../../../../app-server/protocol/server-requests";
export const response = appServerUserInputResponse;
`,
);
expect(messages).toContain("no-restricted-imports");
});
it("keeps generated app-server bindings out of non-exception protocol modules", async () => {