mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Isolate app-server protocol boundaries
This commit is contained in:
parent
2888c0cff0
commit
dd280f74e7
16 changed files with 203 additions and 81 deletions
|
|
@ -57,7 +57,7 @@ Within `src/features/chat/`:
|
|||
|
||||
Keep new code near the state or API it owns. A feature may import another feature only for a capability that feature owns. Feature-neutral behavior belongs in `src/shared/`, `src/domain/`, or `src/app-server/`.
|
||||
|
||||
Generated app-server types should stay behind `src/app-server/` or chat-local app-server integration modules. If a domain, shared, settings, workspace, or UI module needs app-server data, add or reuse a panel-owned projection at the boundary instead of importing generated payload types directly.
|
||||
Generated app-server types should stay behind app-server connection and protocol adapter modules. Chat-local app-server integration modules may consume app-server protocol projections, but not raw generated bindings. If a domain, shared, settings, workspace, or UI module needs app-server data, add or reuse a panel-owned projection at the boundary instead of importing generated payload types directly.
|
||||
|
||||
Chat panel-visible state belongs in `ChatStateStore` and should flow through named reducer actions and the shell-state adapter. Use Preact Signals only in `src/features/chat/panel/shell-state.tsx`; lint enforces this boundary. When a surface needs fewer dependencies, add or reuse a named shell-state projection instead of importing `@preact/signals` elsewhere.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
language js
|
||||
|
||||
or {
|
||||
or {
|
||||
`$client.resumeThread($...)` as $stmt,
|
||||
`$client.threadTurnsList($...)` as $stmt,
|
||||
`$client.forkThread($...)` as $stmt,
|
||||
`$client.rollbackThread($...)` as $stmt
|
||||
} where {
|
||||
$client <: r"^(?:client|[A-Za-z_$][A-Za-z0-9_$]*Client)$",
|
||||
$filename <: r".*/src/features/chat/application/.*",
|
||||
register_diagnostic(span=$stmt, message="Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.", severity="error")
|
||||
},
|
||||
or {
|
||||
`AppServerClient["resumeThread"]` as $stmt,
|
||||
`AppServerClient["threadTurnsList"]` as $stmt,
|
||||
`AppServerClient["forkThread"]` as $stmt,
|
||||
`AppServerClient["rollbackThread"]` as $stmt
|
||||
} where {
|
||||
$filename <: r".*/src/.*",
|
||||
register_diagnostic(span=$stmt, message="Do not expose app-server projection RPC signatures through AppServerClient indexed access types; define a Panel-owned projection type instead.", severity="error")
|
||||
}
|
||||
`$client.resumeThread($...)` as $stmt,
|
||||
`$client.threadTurnsList($...)` as $stmt,
|
||||
`$client.forkThread($...)` as $stmt,
|
||||
`$client.rollbackThread($...)` as $stmt
|
||||
} where {
|
||||
$client <: r"^(?:client|[A-Za-z_$][A-Za-z0-9_$]*Client)$",
|
||||
$filename <: r".*/src/features/chat/application/.*",
|
||||
register_diagnostic(span=$stmt, message="Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.", severity="error")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ or {
|
|||
js_module_reference() as $stmt where {
|
||||
$stmt <: contains `$source` where { $source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$" },
|
||||
not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" },
|
||||
not { $filename <: r".*/src/app-server/protocol/(?:turn|server-requests)\.ts$" },
|
||||
not { $filename <: r".*/src/app-server/protocol/server-requests\.ts$" },
|
||||
register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", severity="error")
|
||||
},
|
||||
`require($source)` as $stmt where {
|
||||
$source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$",
|
||||
not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" },
|
||||
not { $filename <: r".*/src/app-server/protocol/(?:turn|server-requests)\.ts$" },
|
||||
not { $filename <: r".*/src/app-server/protocol/server-requests\.ts$" },
|
||||
register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", severity="error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,29 @@ import {
|
|||
type ThreadConversationSummary,
|
||||
type ThreadTranscriptEntry,
|
||||
} from "../../domain/threads/transcript";
|
||||
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
|
||||
export type TurnItem = GeneratedTurnItem;
|
||||
|
||||
type AppServerUserInput =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "image"; url: string }
|
||||
| { type: "localImage"; path: string }
|
||||
| { type: "text"; text: string; text_elements: AppServerTextElement[] }
|
||||
| { type: "image"; url: string; detail?: "auto" | "low" | "high" | "original" }
|
||||
| { type: "localImage"; path: string; detail?: "auto" | "low" | "high" | "original" }
|
||||
| { type: "mention"; name: string; path: string }
|
||||
| { type: "skill"; name: string; path: string };
|
||||
interface AppServerTextElement {
|
||||
byteRange: { start: number; end: number };
|
||||
placeholder: string | null;
|
||||
}
|
||||
type TurnItemsView = "notLoaded" | "summary" | "full";
|
||||
type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
|
||||
type CommandAction =
|
||||
| { type: "read"; command: string; name: string; path: string | null }
|
||||
| { type: "search"; command: string; query: string | null; path: string | null }
|
||||
| { type: "listFiles"; command: string; path: string | null }
|
||||
| { type: "unknown"; command: string };
|
||||
type WebSearchAction =
|
||||
| { type: "search"; query: string | null; queries: string[] | null }
|
||||
| { type: "openPage"; url: string | null }
|
||||
| { type: "findInPage"; url: string | null; pattern: string | null }
|
||||
| { type: "other" };
|
||||
type HttpCodexErrorInfo =
|
||||
| { httpConnectionFailed: { httpStatusCode: number | null } }
|
||||
| { responseStreamConnectionFailed: { httpStatusCode: number | null } }
|
||||
|
|
@ -41,6 +52,68 @@ interface TurnError {
|
|||
additionalDetails: string | null;
|
||||
}
|
||||
|
||||
interface BaseTurnItem<Type extends string> {
|
||||
type: Type;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type TurnItem =
|
||||
| (BaseTurnItem<"userMessage"> & { clientId: string | null; content: AppServerUserInput[] })
|
||||
| (BaseTurnItem<"hookPrompt"> & { fragments: { text: string; [key: string]: unknown }[] })
|
||||
| (BaseTurnItem<"agentMessage"> & { text: string; phase: string | null; memoryCitation: unknown })
|
||||
| (BaseTurnItem<"plan"> & { text: string })
|
||||
| (BaseTurnItem<"reasoning"> & { summary: string[]; content: string[] })
|
||||
| (BaseTurnItem<"commandExecution"> & {
|
||||
command: string;
|
||||
cwd: string;
|
||||
processId: string | null;
|
||||
source: string;
|
||||
status: string;
|
||||
commandActions: CommandAction[];
|
||||
aggregatedOutput: string | null;
|
||||
exitCode: number | null;
|
||||
durationMs: number | null;
|
||||
})
|
||||
| (BaseTurnItem<"fileChange"> & { changes: { path: string; kind: { type: string }; diff: string }[]; status: string })
|
||||
| (BaseTurnItem<"mcpToolCall"> & {
|
||||
server: string;
|
||||
tool: string;
|
||||
status: string;
|
||||
arguments: unknown;
|
||||
appContext: unknown;
|
||||
pluginId: string | null;
|
||||
result: unknown;
|
||||
error: { message?: string; [key: string]: unknown } | null;
|
||||
durationMs: number | null;
|
||||
})
|
||||
| (BaseTurnItem<"dynamicToolCall"> & {
|
||||
namespace: string | null;
|
||||
tool: string;
|
||||
arguments: unknown;
|
||||
status: string;
|
||||
contentItems: unknown[] | null;
|
||||
success: boolean | null;
|
||||
durationMs: number | null;
|
||||
})
|
||||
| (BaseTurnItem<"collabAgentToolCall"> & {
|
||||
tool: string;
|
||||
status: string;
|
||||
senderThreadId: string;
|
||||
receiverThreadIds: string[];
|
||||
prompt: string | null;
|
||||
model: string | null;
|
||||
reasoningEffort: string | null;
|
||||
agentsStates: Record<string, { status?: string | null; message?: string | null } | undefined>;
|
||||
})
|
||||
| (BaseTurnItem<"subAgentActivity"> & { kind: string; agentThreadId: string; agentPath: string })
|
||||
| (BaseTurnItem<"webSearch"> & { query: string; action: WebSearchAction | null })
|
||||
| (BaseTurnItem<"imageView"> & { path: string })
|
||||
| (BaseTurnItem<"sleep"> & { durationMs: number })
|
||||
| (BaseTurnItem<"imageGeneration"> & { status: string; revisedPrompt: string | null; result: string; savedPath?: string })
|
||||
| (BaseTurnItem<"enteredReviewMode"> & { review: string })
|
||||
| (BaseTurnItem<"exitedReviewMode"> & { review: string })
|
||||
| BaseTurnItem<"contextCompaction">;
|
||||
|
||||
export interface TurnRecord {
|
||||
id: string;
|
||||
items: TurnItem[];
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ import {
|
|||
const THREAD_LIST_PAGE_LIMIT = 100;
|
||||
|
||||
export type ThreadTurnSortDirection = "asc" | "desc";
|
||||
export type ThreadConversationSummaryClient = Pick<AppServerClient, "threadTurnsList">;
|
||||
export type ThreadForkClient = Pick<AppServerClient, "forkThread">;
|
||||
export type ThreadRollbackClient = Pick<AppServerClient, "rollbackThread">;
|
||||
export type ThreadCompactionClient = Pick<AppServerClient, "compactThread">;
|
||||
|
||||
interface ThreadConversationSummaryPage {
|
||||
data: ThreadConversationSummary[];
|
||||
|
|
@ -73,7 +77,7 @@ export async function readThreadForArchiveExport(client: AppServerClient, thread
|
|||
}
|
||||
|
||||
export async function readCompletedConversationSummariesPage(
|
||||
client: AppServerClient,
|
||||
client: ThreadConversationSummaryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit: number,
|
||||
|
|
@ -87,7 +91,7 @@ export async function readCompletedConversationSummariesPage(
|
|||
}
|
||||
|
||||
export async function readReferencedThreadConversationSummaries(
|
||||
client: AppServerClient,
|
||||
client: ThreadConversationSummaryClient,
|
||||
threadId: string,
|
||||
limit = REFERENCED_THREAD_TURN_LIMIT,
|
||||
): Promise<ThreadConversationSummary[]> {
|
||||
|
|
@ -116,16 +120,20 @@ function threadRollbackSnapshotFromAppServerResponse(response: ThreadRollbackRes
|
|||
};
|
||||
}
|
||||
|
||||
export async function rollbackThread(client: AppServerClient, threadId: string, numTurns?: number): Promise<ThreadRollbackSnapshot> {
|
||||
export async function rollbackThread(client: ThreadRollbackClient, threadId: string, numTurns?: number): Promise<ThreadRollbackSnapshot> {
|
||||
const response = numTurns === undefined ? await client.rollbackThread(threadId) : await client.rollbackThread(threadId, numTurns);
|
||||
return threadRollbackSnapshotFromAppServerResponse(response);
|
||||
}
|
||||
|
||||
export async function forkThread(client: AppServerClient, threadId: string, cwd: string): Promise<Thread> {
|
||||
export async function forkThread(client: ThreadForkClient, threadId: string, cwd: string): Promise<Thread> {
|
||||
const response = await client.forkThread(threadId, cwd);
|
||||
return threadFromThreadRecord(response.thread);
|
||||
}
|
||||
|
||||
export async function compactThread(client: ThreadCompactionClient, threadId: string): Promise<void> {
|
||||
await client.compactThread(threadId);
|
||||
}
|
||||
|
||||
export async function restoreArchivedThread(client: AppServerClient, threadId: string): Promise<Thread> {
|
||||
const response = await client.unarchiveThread(threadId);
|
||||
return threadFromThreadRecord(response.thread);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import type { ThreadTurnsPage } from "../../../../domain/threads/history";
|
|||
import type { MessageStreamItem } from "../../domain/message-stream/items";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
|
||||
export type ChatThreadHistoryClient = Pick<AppServerClient, "threadTurnsList">;
|
||||
export type ChatThreadResumeClient = Pick<AppServerClient, "resumeThread">;
|
||||
|
||||
export interface ChatThreadHistoryPage {
|
||||
items: MessageStreamItem[];
|
||||
nextCursor: string | null;
|
||||
|
|
@ -18,7 +21,7 @@ export interface ChatThreadResumeSnapshot {
|
|||
}
|
||||
|
||||
export async function readChatThreadHistoryPage(
|
||||
client: AppServerClient,
|
||||
client: ChatThreadHistoryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit = 20,
|
||||
|
|
@ -34,7 +37,7 @@ function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ChatThreadHi
|
|||
};
|
||||
}
|
||||
|
||||
export async function resumeChatThread(client: AppServerClient, threadId: string, cwd: string): Promise<ChatThreadResumeSnapshot> {
|
||||
export async function resumeChatThread(client: ChatThreadResumeClient, threadId: string, cwd: string): Promise<ChatThreadResumeSnapshot> {
|
||||
const response = await client.resumeThread(threadId, cwd);
|
||||
return {
|
||||
activation: threadActivationSnapshotFromAppServerResponse(response),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { readReferencedThreadConversationSummaries } from "../../../../app-server/threads";
|
||||
import { readReferencedThreadConversationSummaries, type ThreadConversationSummaryClient } from "../../../../app-server/threads";
|
||||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import { type CodexInput, codexTextInputWithAttachments } from "../../../../domain/chat/input";
|
||||
|
|
@ -21,7 +20,7 @@ import { submissionStateSnapshot } from "./submission-state";
|
|||
|
||||
export interface SlashCommandExecutorHost extends SlashCommandExecutionPorts {
|
||||
stateStore: ChatStateStore;
|
||||
currentClient: () => AppServerClient | null;
|
||||
currentClient: () => ThreadConversationSummaryClient | null;
|
||||
codexInput: (text: string) => CodexInput;
|
||||
setStatus: (status: string) => void;
|
||||
}
|
||||
|
|
@ -65,7 +64,7 @@ function supportedReasoningEfforts(state: ReturnType<ChatStateStore["getState"]>
|
|||
|
||||
async function referencedThreadInput(
|
||||
host: SlashCommandExecutorHost,
|
||||
client: AppServerClient,
|
||||
client: ThreadConversationSummaryClient,
|
||||
thread: Thread,
|
||||
message: string,
|
||||
): Promise<ThreadReferenceInput | null> {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { type ChatThreadHistoryPage, readChatThreadHistoryPage } from "../../app-server/threads/projection";
|
||||
import { type ChatThreadHistoryClient, type ChatThreadHistoryPage, readChatThreadHistoryPage } from "../../app-server/threads/projection";
|
||||
import { messageStreamItems } from "../state/message-stream";
|
||||
import type { ChatAction, ChatState } from "../state/root-reducer";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
|
||||
export interface HistoryControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
currentClient: () => AppServerClient | null;
|
||||
currentClient: () => ChatThreadHistoryClient | null;
|
||||
addSystemMessage: (text: string) => void;
|
||||
showLatestPageAtBottom: () => void;
|
||||
setThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
readHistoryPage?: (client: AppServerClient, threadId: string, cursor: string | null, limit: number) => Promise<ChatThreadHistoryPage>;
|
||||
readHistoryPage?: (
|
||||
client: ChatThreadHistoryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit: number,
|
||||
) => Promise<ChatThreadHistoryPage>;
|
||||
}
|
||||
|
||||
type ThreadHistoryLoadLifecycleState = { kind: "idle" } | { kind: "loading"; threadId: string; mode: "latest" | "older" };
|
||||
|
|
@ -110,7 +114,12 @@ export class HistoryController {
|
|||
return this.lifecycle !== load || this.state.activeThread.id !== load.threadId;
|
||||
}
|
||||
|
||||
private readHistoryPage(client: AppServerClient, threadId: string, cursor: string | null, limit: number): Promise<ChatThreadHistoryPage> {
|
||||
private readHistoryPage(
|
||||
client: ChatThreadHistoryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit: number,
|
||||
): Promise<ChatThreadHistoryPage> {
|
||||
return (this.host.readHistoryPage ?? readChatThreadHistoryPage)(client, threadId, cursor, limit);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics";
|
||||
import { type ChatThreadResumeSnapshot, resumeChatThread } from "../../app-server/threads/projection";
|
||||
import { type ChatThreadResumeClient, type ChatThreadResumeSnapshot, resumeChatThread } from "../../app-server/threads/projection";
|
||||
import type { ActiveChatResume, ChatResumeWorkTracker } from "../lifecycle";
|
||||
import { resumedThreadAction } from "../state/actions";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
|
|
@ -14,7 +13,7 @@ export interface ResumeActionsHost {
|
|||
resumeWork: ChatResumeWorkTracker;
|
||||
history: HistoryController;
|
||||
restoration: RestorationController;
|
||||
currentClient: () => AppServerClient | null;
|
||||
currentClient: () => ChatThreadResumeClient | null;
|
||||
ensureConnected: () => Promise<void>;
|
||||
closing: () => boolean;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
|
|
@ -23,7 +22,7 @@ export interface ResumeActionsHost {
|
|||
refreshLiveState: () => void;
|
||||
syncThreadGoal: (threadId: string) => Promise<void>;
|
||||
recoverTokenUsageFromRollout?: (path: string) => Promise<ThreadTokenUsage | null>;
|
||||
resumeFromAppServer?: (client: AppServerClient, threadId: string, cwd: string) => Promise<ChatThreadResumeSnapshot>;
|
||||
resumeFromAppServer?: (client: ChatThreadResumeClient, threadId: string, cwd: string) => Promise<ChatThreadResumeSnapshot>;
|
||||
}
|
||||
|
||||
export interface ResumeActions {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import { forkThread as forkThreadOnAppServer, rollbackThread as rollbackThreadOnAppServer } from "../../../../app-server/threads";
|
||||
import {
|
||||
compactThread as compactThreadOnAppServer,
|
||||
forkThread as forkThreadOnAppServer,
|
||||
rollbackThread as rollbackThreadOnAppServer,
|
||||
type ThreadCompactionClient,
|
||||
type ThreadForkClient,
|
||||
type ThreadRollbackClient,
|
||||
} from "../../../../app-server/threads";
|
||||
import { inheritedForkThreadName } from "../../../../domain/threads/model";
|
||||
import type { ThreadCatalogEvent } from "../../../../workspace/thread-catalog";
|
||||
import type { ThreadOperations } from "../../../threads/thread-operations";
|
||||
|
|
@ -20,8 +26,8 @@ export interface ThreadManagementActionsHost {
|
|||
stateStore: ChatStateStore;
|
||||
vaultPath: string;
|
||||
operations: ThreadOperations;
|
||||
connectedClient: () => Promise<AppServerClient | null>;
|
||||
currentClient: () => AppServerClient | null;
|
||||
connectedClient: () => Promise<ThreadManagementClient | null>;
|
||||
currentClient: () => ThreadManagementClient | null;
|
||||
addSystemMessage: (text: string) => void;
|
||||
setStatus: (status: string) => void;
|
||||
setComposerText: (text: string) => void;
|
||||
|
|
@ -43,11 +49,13 @@ export interface ThreadManagementActions {
|
|||
}
|
||||
|
||||
interface ThreadManagementActionScope {
|
||||
client: AppServerClient;
|
||||
client: ThreadManagementClient;
|
||||
targetThreadId: string;
|
||||
initialActiveThreadId: string | null;
|
||||
}
|
||||
|
||||
type ThreadManagementClient = ThreadCompactionClient & ThreadForkClient & ThreadRollbackClient;
|
||||
|
||||
export function createThreadManagementActions(host: ThreadManagementActionsHost): ThreadManagementActions {
|
||||
return {
|
||||
compactActiveThread: () => compactActiveThread(host),
|
||||
|
|
@ -113,7 +121,7 @@ async function compactThread(host: ThreadManagementActionsHost, threadId: string
|
|||
const scope = await captureThreadManagementActionScope(host, threadId);
|
||||
if (!scope) return;
|
||||
try {
|
||||
await scope.client.compactThread(threadId);
|
||||
await compactThreadOnAppServer(scope.client, threadId);
|
||||
if (!threadManagementScopeStillTargetsOriginalPanel(host, scope)) return;
|
||||
host.addSystemMessage(STATUS_COMPACTION_REQUESTED);
|
||||
host.setStatus(STATUS_COMPACTION_REQUESTED);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ import type { ServerRequest } from "../../src/generated/app-server/ServerRequest
|
|||
import type { ModelListResponse } from "../../src/generated/app-server/v2/ModelListResponse";
|
||||
import type { Thread as AppServerThread } from "../../src/generated/app-server/v2/Thread";
|
||||
import type { ThreadStartResponse } from "../../src/generated/app-server/v2/ThreadStartResponse";
|
||||
import type { TurnStartResponse } from "../../src/generated/app-server/v2/TurnStartResponse";
|
||||
|
||||
type TurnStartResponse = Awaited<ReturnType<EphemeralStructuredTurnClient["startStructuredTurn"]>>;
|
||||
|
||||
describe("runEphemeralStructuredTurn", () => {
|
||||
it("fills completed turn items from item completion notifications", async () => {
|
||||
|
|
@ -376,16 +377,18 @@ function agentMessage(id: string, text: string): TurnItem {
|
|||
}
|
||||
|
||||
function completedItemNotification(threadId: string, turnId: string, item: TurnItem): ServerNotification {
|
||||
type ItemCompletedNotification = Extract<ServerNotification, { method: "item/completed" }>;
|
||||
return {
|
||||
method: "item/completed",
|
||||
params: { threadId, turnId, item, completedAtMs: 1 },
|
||||
params: { threadId, turnId, item, completedAtMs: 1 } as unknown as ItemCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
function turnCompletedNotification(threadId: string, completedTurn: TurnRecord): ServerNotification {
|
||||
type TurnCompletedNotification = Extract<ServerNotification, { method: "turn/completed" }>;
|
||||
return {
|
||||
method: "turn/completed",
|
||||
params: { threadId, turn: completedTurn },
|
||||
params: { threadId, turn: completedTurn } as unknown as TurnCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ describe("generated app-server import boundary", () => {
|
|||
expect(generatedImportPattern.test('export * from "../../src/generated/app-server/v2/Thread";')).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps generated app-server types behind the app-server boundary", async () => {
|
||||
it("keeps generated app-server types behind explicit app-server adapters", async () => {
|
||||
const files = await sourceFiles(sourceRoot);
|
||||
const offenders: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = slashPath(path.relative(repoRoot, file));
|
||||
if (relativePath.startsWith("src/generated/")) continue;
|
||||
if (relativePath.startsWith("src/app-server/")) continue;
|
||||
if (allowsGeneratedAppServerImport(relativePath)) continue;
|
||||
if (generatedImportPattern.test(await readFile(file, "utf8"))) offenders.push(relativePath);
|
||||
}
|
||||
|
||||
|
|
@ -61,3 +61,7 @@ async function sourceFiles(directory: string): Promise<string[]> {
|
|||
function slashPath(value: string): string {
|
||||
return value.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function allowsGeneratedAppServerImport(relativePath: string): boolean {
|
||||
return relativePath.startsWith("src/app-server/connection/") || relativePath === "src/app-server/protocol/server-requests.ts";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type InitializeResponse = ServerInitialization;
|
|||
type ModelListResponse = Awaited<ReturnType<AppServerClient["listModels"]>>;
|
||||
type ThreadStartResponse = Awaited<ReturnType<AppServerClient["startEphemeralThread"]>>;
|
||||
type Turn = TurnRecord;
|
||||
type TurnStartResponse = Awaited<ReturnType<AppServerClient["startStructuredTurn"]>>;
|
||||
type TurnStartResponse = Awaited<ReturnType<EphemeralStructuredTurnClient["startStructuredTurn"]>>;
|
||||
|
||||
describe("thread title", () => {
|
||||
it("builds title context from a conversation summary", () => {
|
||||
|
|
@ -325,16 +325,18 @@ function assistantMessage(id: string, text: string): TurnItem {
|
|||
}
|
||||
|
||||
function completedItemNotification(threadId: string, turnId: string, item: TurnItem): ServerNotification {
|
||||
type ItemCompletedNotification = Extract<ServerNotification, { method: "item/completed" }>;
|
||||
return {
|
||||
method: "item/completed",
|
||||
params: { threadId, turnId, item, completedAtMs: 1 },
|
||||
params: { threadId, turnId, item, completedAtMs: 1 } as unknown as ItemCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
function turnCompletedNotification(threadId: string, completedTurn: Turn): ServerNotification {
|
||||
type TurnCompletedNotification = Extract<ServerNotification, { method: "turn/completed" }>;
|
||||
return {
|
||||
method: "turn/completed",
|
||||
params: { threadId, turn: completedTurn },
|
||||
params: { threadId, turn: completedTurn } as unknown as TurnCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1557,7 +1557,10 @@ describe("ChatInboundHandler", () => {
|
|||
|
||||
handler.handleNotification({
|
||||
method: "turn/completed",
|
||||
params: { threadId: "thread-active", turn },
|
||||
params: {
|
||||
threadId: "thread-active",
|
||||
turn: turn as unknown as Extract<ServerNotification, { method: "turn/completed" }>["params"]["turn"],
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/completed" }>);
|
||||
|
||||
expect(maybeNameThread).toHaveBeenCalledWith("thread-active", "turn-active", {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ type InitializeResponse = ServerInitialization;
|
|||
type ModelListResponse = Awaited<ReturnType<AppServerClient["listModels"]>>;
|
||||
type ThreadStartResponse = Awaited<ReturnType<AppServerClient["startEphemeralThread"]>>;
|
||||
type Turn = TurnRecord;
|
||||
type TurnStartResponse = Awaited<ReturnType<AppServerClient["startStructuredTurn"]>>;
|
||||
type SelectionRewriteClientFactory = NonNullable<Parameters<typeof runSelectionRewrite>[0]["clientFactory"]>;
|
||||
type SelectionRewriteClient = ReturnType<SelectionRewriteClientFactory>;
|
||||
type TurnStartResponse = Awaited<ReturnType<SelectionRewriteClient["startStructuredTurn"]>>;
|
||||
|
||||
installObsidianDomShims();
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
|
@ -900,16 +900,18 @@ function agentDeltaNotification(threadId: string, turnId: string, delta: string)
|
|||
}
|
||||
|
||||
function completedItemNotification(threadId: string, turnId: string, item: TurnItem): ServerNotification {
|
||||
type ItemCompletedNotification = Extract<ServerNotification, { method: "item/completed" }>;
|
||||
return {
|
||||
method: "item/completed",
|
||||
params: { threadId, turnId, item, completedAtMs: 1 },
|
||||
params: { threadId, turnId, item, completedAtMs: 1 } as unknown as ItemCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
function turnCompletedNotification(threadId: string, completedTurn: Turn): ServerNotification {
|
||||
type TurnCompletedNotification = Extract<ServerNotification, { method: "turn/completed" }>;
|
||||
return {
|
||||
method: "turn/completed",
|
||||
params: { threadId, turn: completedTurn },
|
||||
params: { threadId, turn: completedTurn } as unknown as TurnCompletedNotification["params"],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -329,11 +329,24 @@ export interface RuntimeOverrideModelClient {
|
|||
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
|
||||
export type TurnItem = GeneratedTurnItem;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/app-server/protocol/server-requests.ts"),
|
||||
`
|
||||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
|
||||
export type Request = ServerRequest;
|
||||
`.trimStart(),
|
||||
);
|
||||
|
||||
const report = biomeLint(
|
||||
["src/app-server/protocol/request-input.ts", "src/app-server/services/runtime-overrides.ts", "src/app-server/protocol/turn.ts"],
|
||||
[
|
||||
"src/app-server/protocol/request-input.ts",
|
||||
"src/app-server/services/runtime-overrides.ts",
|
||||
"src/app-server/protocol/turn.ts",
|
||||
"src/app-server/protocol/server-requests.ts",
|
||||
],
|
||||
cwd,
|
||||
);
|
||||
|
||||
|
|
@ -343,7 +356,10 @@ export type TurnItem = GeneratedTurnItem;
|
|||
expect(pluginMessages(report, "src/app-server/services/runtime-overrides.ts")).toEqual([
|
||||
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
|
||||
]);
|
||||
expect(pluginDiagnostics(report, "src/app-server/protocol/turn.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([
|
||||
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
|
||||
]);
|
||||
expect(pluginDiagnostics(report, "src/app-server/protocol/server-requests.ts")).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps lower-level source independent from connection and feature layers", async () => {
|
||||
|
|
@ -419,7 +435,7 @@ export const format = formatDate;
|
|||
expect(pluginDiagnostics(report, "src/domain/threads/format.ts")).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps generated app-server import shapes behind narrow aliases and protocol exceptions", async () => {
|
||||
it("keeps generated app-server import shapes behind narrow aliases and server request exceptions", async () => {
|
||||
const cwd = await tempBiomeWorkspace(["no-generated-app-server-import-shapes.grit"]);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/app-server/connection/thread.ts"),
|
||||
|
|
@ -446,13 +462,15 @@ export type ConnectionThread = ThreadRecord;
|
|||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/app-server/protocol/turn.ts"),
|
||||
path.join(cwd, "src/app-server/connection/import-type-thread.ts"),
|
||||
`
|
||||
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
|
||||
import type { UserInput } from "../../generated/app-server/v2/UserInput";
|
||||
|
||||
export type TurnItem = GeneratedTurnItem;
|
||||
export type Input = UserInput;
|
||||
export type ConnectionThread = import("../../generated/app-server/v2/Thread").Thread;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/app-server/connection/export-thread.ts"),
|
||||
`
|
||||
export type { Thread } from "../../generated/app-server/v2/Thread";
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
|
|
@ -479,7 +497,8 @@ export const loadedParams = params;
|
|||
"src/app-server/connection/thread.ts",
|
||||
"src/app-server/connection/aliased-thread.ts",
|
||||
"src/app-server/connection/record-thread.ts",
|
||||
"src/app-server/protocol/turn.ts",
|
||||
"src/app-server/connection/import-type-thread.ts",
|
||||
"src/app-server/connection/export-thread.ts",
|
||||
"src/app-server/protocol/server-requests.ts",
|
||||
],
|
||||
cwd,
|
||||
|
|
@ -492,8 +511,11 @@ export const loadedParams = params;
|
|||
expect(pluginMessages(report, "src/app-server/connection/record-thread.ts")).toEqual([
|
||||
"Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
|
||||
]);
|
||||
expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([
|
||||
"Only generated ThreadItem is allowed in the turn protocol exception. Model other turn payload shapes locally.",
|
||||
expect(pluginMessages(report, "src/app-server/connection/import-type-thread.ts")).toEqual([
|
||||
"Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
|
||||
]);
|
||||
expect(pluginMessages(report, "src/app-server/connection/export-thread.ts")).toEqual([
|
||||
"Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
|
||||
]);
|
||||
expect(pluginMessages(report, "src/app-server/protocol/server-requests.ts")).toEqual([
|
||||
"Only generated RequestId and ServerRequest are allowed in the server request protocol exception.",
|
||||
|
|
@ -530,7 +552,7 @@ export async function read(client: AppServerClient): Promise<void> {
|
|||
`
|
||||
import type { AppServerClient } from "../../../app-server/connection/client";
|
||||
|
||||
type Resume = AppServerClient["resumeThread"];
|
||||
export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
|
||||
`.trimStart(),
|
||||
);
|
||||
|
||||
|
|
@ -543,9 +565,7 @@ type Resume = AppServerClient["resumeThread"];
|
|||
"Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.",
|
||||
]);
|
||||
expect(pluginDiagnostics(report, "src/app-server/threads.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/features/chat/host/connection-bundle.ts")).toEqual([
|
||||
"Do not expose app-server projection RPC signatures through AppServerClient indexed access types; define a Panel-owned projection type instead.",
|
||||
]);
|
||||
expect(pluginDiagnostics(report, "src/features/chat/host/connection-bundle.ts")).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps imperative DOM bridges behind filename suffixes", async () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue