mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Collapse redundant design-review indirection
Inline low-value wrappers around app-server transports, runtime resolution, session lifecycle wiring, message-stream projection, and settings lifecycle state.
This commit is contained in:
parent
b4e4f6251e
commit
ceb85bd251
32 changed files with 653 additions and 875 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
import type { ServerNotification } from "../../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
import { appServerInitializationFromResponse } from "../protocol/initialization";
|
||||
import { AppServerClient, type AppServerClientHandlers } from "./client";
|
||||
|
||||
export interface ConnectionManagerHandlers {
|
||||
|
|
@ -67,7 +66,7 @@ export class ConnectionManager {
|
|||
async connect(handlers: ConnectionManagerHandlers): Promise<ServerInitialization> {
|
||||
const currentClient = this.currentClient();
|
||||
if (currentClient) {
|
||||
return appServerInitializationFromResponse(currentClient.initializeResponse);
|
||||
return serverInitializationFromResponse(currentClient.initializeResponse);
|
||||
}
|
||||
if (this.state.kind === "connecting" && this.stateMatchesCurrentContext(this.state)) return this.state.promise;
|
||||
if (this.state.kind === "connecting" || this.state.kind === "connected") this.disconnect();
|
||||
|
|
@ -102,7 +101,7 @@ export class ConnectionManager {
|
|||
throw new StaleConnectionError();
|
||||
}
|
||||
this.state = { kind: "connected", generation, codexPath, cwd, client };
|
||||
return appServerInitializationFromResponse(response);
|
||||
return serverInitializationFromResponse(response);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (this.isStale(generation)) {
|
||||
|
|
@ -139,3 +138,12 @@ export class ConnectionManager {
|
|||
return state.codexPath === this.codexPath() && state.cwd === this.cwd;
|
||||
}
|
||||
}
|
||||
|
||||
function serverInitializationFromResponse(response: ServerInitialization): ServerInitialization {
|
||||
return {
|
||||
userAgent: response.userAgent,
|
||||
codexHome: response.codexHome,
|
||||
platformFamily: response.platformFamily,
|
||||
platformOs: response.platformOs,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
|
||||
interface AppServerInitializeResponse {
|
||||
userAgent: string;
|
||||
codexHome: string;
|
||||
platformFamily: string;
|
||||
platformOs: string;
|
||||
}
|
||||
|
||||
export function appServerInitializationFromResponse(response: AppServerInitializeResponse): ServerInitialization {
|
||||
return {
|
||||
userAgent: response.userAgent,
|
||||
codexHome: response.codexHome,
|
||||
platformFamily: response.platformFamily,
|
||||
platformOs: response.platformOs,
|
||||
};
|
||||
}
|
||||
|
|
@ -6,10 +6,6 @@ export interface ObservedResult<T> {
|
|||
|
||||
export type ObservedResultListener<T> = (result: ObservedResult<T>) => void;
|
||||
|
||||
export function observedValue<T>(result: ObservedResult<T>): T | null {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export function observedInitialLoading<T>(result: ObservedResult<T>, currentValue: T | null | undefined): boolean {
|
||||
return currentValue == null && result.isFetching;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@ import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../applicatio
|
|||
import type { ThreadHistoryTransport, ThreadResumeTransport } from "../application/threads/thread-loading-transport";
|
||||
import type { ThreadMutationTransport } from "../application/threads/thread-mutation-transport";
|
||||
import { createThreadReferenceResolver, type ThreadReferenceResolver } from "./thread-reference-resolver";
|
||||
import { createChatThreadGoalReadTransport, createChatThreadGoalTransport } from "./transports/goal-transport";
|
||||
import { createChatThreadHistoryTransport, createChatThreadResumeTransport } from "./transports/thread-loading-transport";
|
||||
import { createChatThreadMutationTransport } from "./transports/thread-mutation-transport";
|
||||
import { createChatRuntimeSettingsTransport } from "./transports/thread-settings-transport";
|
||||
import { createChatTurnTransport } from "./transports/turn-transport";
|
||||
import {
|
||||
createChatRuntimeSettingsTransport,
|
||||
createChatThreadGoalReadTransport,
|
||||
createChatThreadGoalTransport,
|
||||
createChatThreadHistoryTransport,
|
||||
createChatThreadMutationTransport,
|
||||
createChatThreadResumeTransport,
|
||||
createChatTurnTransport,
|
||||
} from "./transports/session-transports";
|
||||
|
||||
export interface ChatAppServerGatewayHost {
|
||||
vaultPath: string;
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
|
||||
export interface CurrentChatAppServerClientHost {
|
||||
currentClient(): AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface ConnectedChatAppServerClientHost extends CurrentChatAppServerClientHost {
|
||||
connectedClient(): Promise<AppServerClient | null>;
|
||||
}
|
||||
|
||||
export function chatAppServerClientIsStale(host: CurrentChatAppServerClientHost, client: AppServerClient): boolean {
|
||||
return host.currentClient() !== client;
|
||||
}
|
||||
|
||||
export async function withConnectedChatAppServerClient<T>(
|
||||
host: ConnectedChatAppServerClientHost,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T | null> {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return null;
|
||||
const result = await operation(client);
|
||||
return chatAppServerClientIsStale(host, client) ? null : result;
|
||||
}
|
||||
|
||||
export async function withCurrentChatAppServerClient<T>(
|
||||
host: CurrentChatAppServerClientHost,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T | null> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return null;
|
||||
const result = await operation(client);
|
||||
return chatAppServerClientIsStale(host, client) ? null : result;
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { clearThreadGoal, readThreadGoal, recordThreadGoalUserMessage, setThreadGoal } from "../../../../app-server/services/threads";
|
||||
import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../../application/threads/goal-transport";
|
||||
import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "./client-scope";
|
||||
import { chatAppServerClientIsStale, withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "./client-scope";
|
||||
|
||||
export function createChatThreadGoalReadTransport(host: CurrentChatAppServerClientHost): ThreadGoalReadTransport {
|
||||
return {
|
||||
readThreadGoal: (threadId) => readThreadGoalFromCurrentClient(host, threadId),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadGoalTransport(host: ConnectedChatAppServerClientHost): ThreadGoalTransport {
|
||||
return {
|
||||
ensureConnected: async () => (await host.connectedClient()) !== null,
|
||||
readThreadGoal: (threadId) => readThreadGoalFromCurrentClient(host, threadId),
|
||||
setThreadGoal: async (threadId, params) => {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return undefined;
|
||||
const goal = await setThreadGoal(client, threadId, params);
|
||||
return chatAppServerClientIsStale(host, client) ? undefined : goal;
|
||||
},
|
||||
clearThreadGoal: async (threadId) => {
|
||||
const result = await withConnectedChatAppServerClient(host, async (client) => {
|
||||
await clearThreadGoal(client, threadId);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
recordThreadGoalUserMessage: async (threadId, objective) => {
|
||||
const result = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await recordThreadGoalUserMessage(client, threadId, objective);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function readThreadGoalFromCurrentClient(
|
||||
host: CurrentChatAppServerClientHost,
|
||||
threadId: string,
|
||||
): ReturnType<ThreadGoalReadTransport["readThreadGoal"]> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return undefined;
|
||||
const goal = await readThreadGoal(client, threadId);
|
||||
return chatAppServerClientIsStale(host, client) ? undefined : goal;
|
||||
}
|
||||
231
src/features/chat/app-server/transports/session-transports.ts
Normal file
231
src/features/chat/app-server/transports/session-transports.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import type { AppServerClient } from "../../../../app-server/connection/client";
|
||||
import type { AppServerRequestClient } from "../../../../app-server/services/request-client";
|
||||
import {
|
||||
clearThreadGoal,
|
||||
compactThread,
|
||||
forkThread,
|
||||
listThreadTurns,
|
||||
readThreadGoal,
|
||||
recordThreadGoalUserMessage,
|
||||
resumeThread,
|
||||
rollbackThread,
|
||||
setThreadGoal,
|
||||
threadActivationSnapshotFromAppServerResponse,
|
||||
updateThreadSettings,
|
||||
} from "../../../../app-server/services/threads";
|
||||
import { interruptTurn, startTurn, steerTurn } from "../../../../app-server/services/turns";
|
||||
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
|
||||
import type { ThreadTurnsPage } from "../../../../domain/threads/history";
|
||||
import type { ChatTurnTransport } from "../../application/conversation/turn-transport";
|
||||
import type { RuntimeSettingsTransport } from "../../application/runtime/settings-transport";
|
||||
import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../../application/threads/goal-transport";
|
||||
import type {
|
||||
ThreadHistoryPage,
|
||||
ThreadHistoryTransport,
|
||||
ThreadResumeSnapshot,
|
||||
ThreadResumeTransport,
|
||||
} from "../../application/threads/thread-loading-transport";
|
||||
import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
|
||||
interface CurrentChatAppServerClientHost {
|
||||
currentClient(): AppServerClient | null;
|
||||
}
|
||||
|
||||
interface ConnectedChatAppServerClientHost extends CurrentChatAppServerClientHost {
|
||||
connectedClient(): Promise<AppServerClient | null>;
|
||||
}
|
||||
|
||||
interface ChatAppServerTransportHost extends ConnectedChatAppServerClientHost {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
type ChatThreadHistoryClient = AppServerRequestClient;
|
||||
type ChatThreadResumeClient = AppServerRequestClient;
|
||||
|
||||
interface AppServerThreadTurnsPage {
|
||||
readonly data: ThreadTurnsPage["turns"];
|
||||
readonly nextCursor: string | null;
|
||||
}
|
||||
|
||||
export function createChatTurnTransport(host: ChatAppServerTransportHost): ChatTurnTransport {
|
||||
return {
|
||||
ensureConnected: async () => (await host.connectedClient()) !== null,
|
||||
startTurn: (request) =>
|
||||
withCurrentChatAppServerClient(host, async (client) => {
|
||||
const response = await startTurn(client, {
|
||||
threadId: request.threadId,
|
||||
cwd: host.vaultPath,
|
||||
input: request.input,
|
||||
clientUserMessageId: request.clientUserMessageId,
|
||||
});
|
||||
return { turnId: response.turn.id };
|
||||
}),
|
||||
steerTurn: async (request) => {
|
||||
const steered = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await steerTurn(client, request.threadId, request.turnId, request.input, request.clientUserMessageId);
|
||||
return true;
|
||||
});
|
||||
return steered ?? false;
|
||||
},
|
||||
interruptTurn: async (threadId, turnId) => {
|
||||
const interrupted = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await interruptTurn(client, threadId, turnId);
|
||||
return true;
|
||||
});
|
||||
return interrupted ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatRuntimeSettingsTransport(host: CurrentChatAppServerClientHost): RuntimeSettingsTransport {
|
||||
return {
|
||||
updateThreadSettings: async (threadId: string, update: RuntimeSettingsPatch) => {
|
||||
const result = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await updateThreadSettings(client, threadId, update);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadHistoryTransport(host: CurrentChatAppServerClientHost): ThreadHistoryTransport {
|
||||
return {
|
||||
readHistoryPage: (threadId, cursor, limit): Promise<ThreadHistoryPage | null> =>
|
||||
withCurrentChatAppServerClient(host, (client) => readChatThreadHistoryPage(client, threadId, cursor, limit)),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadResumeTransport(host: ChatAppServerTransportHost): ThreadResumeTransport {
|
||||
return {
|
||||
resumeThread: (threadId): Promise<ThreadResumeSnapshot | null> =>
|
||||
withConnectedChatAppServerClient(host, (client) => resumeChatThread(client, threadId, host.vaultPath)),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadMutationTransport(host: ChatAppServerTransportHost): ThreadMutationTransport {
|
||||
return {
|
||||
compactThread: async (threadId) => {
|
||||
const result = await withConnectedChatAppServerClient(host, async (client) => {
|
||||
await compactThread(client, threadId);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
forkThread: (threadId) => withConnectedChatAppServerClient(host, (client) => forkThread(client, threadId, host.vaultPath)),
|
||||
rollbackForkedThread: (threadId, turnsToDrop) =>
|
||||
withConnectedChatAppServerClient(host, async (client) => (await rollbackThread(client, threadId, turnsToDrop)).thread),
|
||||
rollbackThread: (threadId) =>
|
||||
withConnectedChatAppServerClient(host, async (client): Promise<ThreadRollbackSnapshot> => {
|
||||
const snapshot = await rollbackThread(client, threadId);
|
||||
return {
|
||||
thread: snapshot.thread,
|
||||
cwd: snapshot.cwd,
|
||||
items: messageStreamItemsFromTurns(snapshot.turns),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadGoalReadTransport(host: CurrentChatAppServerClientHost): ThreadGoalReadTransport {
|
||||
return {
|
||||
readThreadGoal: (threadId) => readThreadGoalFromCurrentClient(host, threadId),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadGoalTransport(host: ConnectedChatAppServerClientHost): ThreadGoalTransport {
|
||||
return {
|
||||
ensureConnected: async () => (await host.connectedClient()) !== null,
|
||||
readThreadGoal: (threadId) => readThreadGoalFromCurrentClient(host, threadId),
|
||||
setThreadGoal: async (threadId, params) => {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return undefined;
|
||||
const goal = await setThreadGoal(client, threadId, params);
|
||||
return chatAppServerClientIsStale(host, client) ? undefined : goal;
|
||||
},
|
||||
clearThreadGoal: async (threadId) => {
|
||||
const result = await withConnectedChatAppServerClient(host, async (client) => {
|
||||
await clearThreadGoal(client, threadId);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
recordThreadGoalUserMessage: async (threadId, objective) => {
|
||||
const result = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await recordThreadGoalUserMessage(client, threadId, objective);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function chatAppServerClientIsStale(host: CurrentChatAppServerClientHost, client: AppServerClient): boolean {
|
||||
return host.currentClient() !== client;
|
||||
}
|
||||
|
||||
async function withConnectedChatAppServerClient<T>(
|
||||
host: ConnectedChatAppServerClientHost,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T | null> {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return null;
|
||||
const result = await operation(client);
|
||||
return chatAppServerClientIsStale(host, client) ? null : result;
|
||||
}
|
||||
|
||||
async function withCurrentChatAppServerClient<T>(
|
||||
host: CurrentChatAppServerClientHost,
|
||||
operation: (client: AppServerClient) => Promise<T>,
|
||||
): Promise<T | null> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return null;
|
||||
const result = await operation(client);
|
||||
return chatAppServerClientIsStale(host, client) ? null : result;
|
||||
}
|
||||
|
||||
async function readChatThreadHistoryPage(
|
||||
client: ChatThreadHistoryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit = 20,
|
||||
): Promise<ThreadHistoryPage> {
|
||||
return chatThreadHistoryPageFromTurnsPage(threadTurnsPageFromAppServerPage(await listThreadTurns(client, threadId, cursor, limit)));
|
||||
}
|
||||
|
||||
function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ThreadHistoryPage {
|
||||
return {
|
||||
items: messageStreamItemsFromTurns(page.turns),
|
||||
nextCursor: page.nextCursor,
|
||||
hadTurns: page.turns.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function resumeChatThread(client: ChatThreadResumeClient, threadId: string, cwd: string): Promise<ThreadResumeSnapshot> {
|
||||
const response = await resumeThread(client, threadId, cwd);
|
||||
return {
|
||||
activation: threadActivationSnapshotFromAppServerResponse(response),
|
||||
rolloutPath: response.thread.path,
|
||||
initialHistoryPage: response.initialTurnsPage
|
||||
? chatThreadHistoryPageFromTurnsPage(threadTurnsPageFromAppServerPage(response.initialTurnsPage))
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function readThreadGoalFromCurrentClient(
|
||||
host: CurrentChatAppServerClientHost,
|
||||
threadId: string,
|
||||
): ReturnType<ThreadGoalReadTransport["readThreadGoal"]> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return undefined;
|
||||
const goal = await readThreadGoal(client, threadId);
|
||||
return chatAppServerClientIsStale(host, client) ? undefined : goal;
|
||||
}
|
||||
|
||||
function threadTurnsPageFromAppServerPage(page: AppServerThreadTurnsPage): ThreadTurnsPage {
|
||||
return {
|
||||
turns: page.data,
|
||||
nextCursor: page.nextCursor,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import type { AppServerRequestClient } from "../../../../app-server/services/request-client";
|
||||
import { listThreadTurns, resumeThread, threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/services/threads";
|
||||
import type { ThreadTurnsPage } from "../../../../domain/threads/history";
|
||||
import type {
|
||||
ThreadHistoryPage,
|
||||
ThreadHistoryTransport,
|
||||
ThreadResumeSnapshot,
|
||||
ThreadResumeTransport,
|
||||
} from "../../application/threads/thread-loading-transport";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "./client-scope";
|
||||
import { withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "./client-scope";
|
||||
|
||||
type ChatThreadHistoryClient = AppServerRequestClient;
|
||||
type ChatThreadResumeClient = AppServerRequestClient;
|
||||
|
||||
interface AppServerThreadTurnsPage {
|
||||
readonly data: ThreadTurnsPage["turns"];
|
||||
readonly nextCursor: string | null;
|
||||
}
|
||||
|
||||
interface ChatThreadResumeTransportHost extends ConnectedChatAppServerClientHost {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function createChatThreadHistoryTransport(host: CurrentChatAppServerClientHost): ThreadHistoryTransport {
|
||||
return {
|
||||
readHistoryPage: (threadId, cursor, limit): Promise<ThreadHistoryPage | null> =>
|
||||
withCurrentChatAppServerClient(host, (client) => readChatThreadHistoryPage(client, threadId, cursor, limit)),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChatThreadResumeTransport(host: ChatThreadResumeTransportHost): ThreadResumeTransport {
|
||||
return {
|
||||
resumeThread: (threadId): Promise<ThreadResumeSnapshot | null> =>
|
||||
withConnectedChatAppServerClient(host, (client) => resumeChatThread(client, threadId, host.vaultPath)),
|
||||
};
|
||||
}
|
||||
|
||||
async function readChatThreadHistoryPage(
|
||||
client: ChatThreadHistoryClient,
|
||||
threadId: string,
|
||||
cursor: string | null,
|
||||
limit = 20,
|
||||
): Promise<ThreadHistoryPage> {
|
||||
return chatThreadHistoryPageFromTurnsPage(threadTurnsPageFromAppServerPage(await listThreadTurns(client, threadId, cursor, limit)));
|
||||
}
|
||||
|
||||
function chatThreadHistoryPageFromTurnsPage(page: ThreadTurnsPage): ThreadHistoryPage {
|
||||
return {
|
||||
items: messageStreamItemsFromTurns(page.turns),
|
||||
nextCursor: page.nextCursor,
|
||||
hadTurns: page.turns.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function resumeChatThread(client: ChatThreadResumeClient, threadId: string, cwd: string): Promise<ThreadResumeSnapshot> {
|
||||
const response = await resumeThread(client, threadId, cwd);
|
||||
return {
|
||||
activation: threadActivationSnapshotFromAppServerResponse(response),
|
||||
rolloutPath: response.thread.path,
|
||||
initialHistoryPage: response.initialTurnsPage
|
||||
? chatThreadHistoryPageFromTurnsPage(threadTurnsPageFromAppServerPage(response.initialTurnsPage))
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function threadTurnsPageFromAppServerPage(page: AppServerThreadTurnsPage): ThreadTurnsPage {
|
||||
return {
|
||||
turns: page.data,
|
||||
nextCursor: page.nextCursor,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { compactThread, forkThread, rollbackThread } from "../../../../app-server/services/threads";
|
||||
import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport";
|
||||
import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items";
|
||||
import type { ConnectedChatAppServerClientHost } from "./client-scope";
|
||||
import { withConnectedChatAppServerClient } from "./client-scope";
|
||||
|
||||
interface ChatThreadMutationTransportHost extends ConnectedChatAppServerClientHost {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function createChatThreadMutationTransport(host: ChatThreadMutationTransportHost): ThreadMutationTransport {
|
||||
return {
|
||||
compactThread: async (threadId) => {
|
||||
const result = await withConnectedChatAppServerClient(host, async (client) => {
|
||||
await compactThread(client, threadId);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
forkThread: (threadId) => withConnectedChatAppServerClient(host, (client) => forkThread(client, threadId, host.vaultPath)),
|
||||
rollbackForkedThread: (threadId, turnsToDrop) =>
|
||||
withConnectedChatAppServerClient(host, async (client) => (await rollbackThread(client, threadId, turnsToDrop)).thread),
|
||||
rollbackThread: (threadId) =>
|
||||
withConnectedChatAppServerClient(host, async (client): Promise<ThreadRollbackSnapshot> => {
|
||||
const snapshot = await rollbackThread(client, threadId);
|
||||
return {
|
||||
thread: snapshot.thread,
|
||||
cwd: snapshot.cwd,
|
||||
items: messageStreamItemsFromTurns(snapshot.turns),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { updateThreadSettings } from "../../../../app-server/services/threads";
|
||||
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
|
||||
import type { RuntimeSettingsTransport } from "../../application/runtime/settings-transport";
|
||||
import type { CurrentChatAppServerClientHost } from "./client-scope";
|
||||
import { withCurrentChatAppServerClient } from "./client-scope";
|
||||
|
||||
export function createChatRuntimeSettingsTransport(host: CurrentChatAppServerClientHost): RuntimeSettingsTransport {
|
||||
return {
|
||||
updateThreadSettings: async (threadId: string, update: RuntimeSettingsPatch) => {
|
||||
const result = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await updateThreadSettings(client, threadId, update);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { interruptTurn, startTurn, steerTurn } from "../../../../app-server/services/turns";
|
||||
import type { ChatTurnTransport } from "../../application/conversation/turn-transport";
|
||||
import type { ConnectedChatAppServerClientHost } from "./client-scope";
|
||||
import { withCurrentChatAppServerClient } from "./client-scope";
|
||||
|
||||
interface ChatTurnTransportHost extends ConnectedChatAppServerClientHost {
|
||||
vaultPath: string;
|
||||
}
|
||||
|
||||
export function createChatTurnTransport(host: ChatTurnTransportHost): ChatTurnTransport {
|
||||
return {
|
||||
ensureConnected: async () => (await host.connectedClient()) !== null,
|
||||
startTurn: (request) =>
|
||||
withCurrentChatAppServerClient(host, async (client) => {
|
||||
const response = await startTurn(client, {
|
||||
threadId: request.threadId,
|
||||
cwd: host.vaultPath,
|
||||
input: request.input,
|
||||
clientUserMessageId: request.clientUserMessageId,
|
||||
});
|
||||
return { turnId: response.turn.id };
|
||||
}),
|
||||
steerTurn: async (request) => {
|
||||
const steered = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await steerTurn(client, request.threadId, request.turnId, request.input, request.clientUserMessageId);
|
||||
return true;
|
||||
});
|
||||
return steered ?? false;
|
||||
},
|
||||
interruptTurn: async (threadId, turnId) => {
|
||||
const interrupted = await withCurrentChatAppServerClient(host, async (client) => {
|
||||
await interruptTurn(client, threadId, turnId);
|
||||
return true;
|
||||
});
|
||||
return interrupted ?? false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics";
|
||||
import type { ChatStateStore } from "../state/store";
|
||||
import { createActiveThreadIdentitySync } from "./active-thread-identity-sync";
|
||||
import type { GoalActions } from "./goal-actions";
|
||||
import type { HistoryController } from "./history-controller";
|
||||
import { RestorationController } from "./restoration-controller";
|
||||
import { createResumeActions, type ResumeActions } from "./resume-actions";
|
||||
import type { ChatResumeWorkTracker } from "./resume-work";
|
||||
import type { ThreadResumeTransport } from "./thread-loading-transport";
|
||||
|
||||
export interface ThreadLifecyclePartsContext {
|
||||
stateStore: ChatStateStore;
|
||||
resumeTransport: ThreadResumeTransport;
|
||||
lifecycle: {
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
history: HistoryController;
|
||||
invalidateThreadWork: () => void;
|
||||
getClosing: () => boolean;
|
||||
recoverTokenUsageFromRollout?: (path: string) => Promise<ThreadTokenUsage | null>;
|
||||
};
|
||||
thread: {
|
||||
notifyIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
};
|
||||
status: {
|
||||
set: (status: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
};
|
||||
liveState: {
|
||||
refresh: () => void;
|
||||
};
|
||||
goals: GoalActions;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ThreadLifecycleParts {
|
||||
history: HistoryController;
|
||||
restoration: RestorationController;
|
||||
resume: ResumeActions;
|
||||
identity: ReturnType<typeof createActiveThreadIdentitySync>;
|
||||
}
|
||||
|
||||
export function createThreadLifecycleParts(context: ThreadLifecyclePartsContext): ThreadLifecycleParts {
|
||||
const { stateStore, resumeTransport, lifecycle, thread, status, liveState, goals, resetThreadTurnPresence } = context;
|
||||
const { resumeWork, history, invalidateThreadWork } = lifecycle;
|
||||
const restoration = new RestorationController({
|
||||
invalidateThreadWork,
|
||||
setStatus: status.set,
|
||||
refreshTabHeader: thread.refreshTabHeader,
|
||||
});
|
||||
const resume = createResumeActions({
|
||||
stateStore,
|
||||
resumeWork,
|
||||
history,
|
||||
restoration,
|
||||
resumeTransport,
|
||||
closing: lifecycle.getClosing,
|
||||
resetThreadTurnPresence,
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
refreshLiveState: liveState.refresh,
|
||||
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
|
||||
...(lifecycle.recoverTokenUsageFromRollout ? { recoverTokenUsageFromRollout: lifecycle.recoverTokenUsageFromRollout } : {}),
|
||||
});
|
||||
const identity = createActiveThreadIdentitySync({
|
||||
stateStore,
|
||||
restoration,
|
||||
invalidateThreadWork,
|
||||
resetThreadTurnPresence,
|
||||
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
|
||||
refreshTabHeader: thread.refreshTabHeader,
|
||||
});
|
||||
|
||||
return {
|
||||
history,
|
||||
restoration,
|
||||
resume,
|
||||
identity,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { effectiveCollaborationMode } from "./intent";
|
||||
import type { RuntimeSnapshot } from "./snapshot";
|
||||
|
||||
export interface CollaborationModeResolution {
|
||||
readonly active: RuntimeSnapshot["active"]["collaborationMode"];
|
||||
readonly pending: RuntimeSnapshot["pending"]["collaborationMode"];
|
||||
readonly confirmed: NonNullable<RuntimeSnapshot["active"]["collaborationMode"]>;
|
||||
readonly effective: NonNullable<RuntimeSnapshot["active"]["collaborationMode"]>;
|
||||
readonly dirty: boolean;
|
||||
readonly blockedReason: "missing-model" | null;
|
||||
}
|
||||
|
||||
export function resolveCollaborationMode(snapshot: RuntimeSnapshot, model: string | null): CollaborationModeResolution {
|
||||
const active = snapshot.active.collaborationMode;
|
||||
const pending = snapshot.pending.collaborationMode;
|
||||
const confirmed = effectiveCollaborationMode(active);
|
||||
const effective = pending.kind === "set" ? pending.value : confirmed;
|
||||
const dirty = pending.kind === "set";
|
||||
return {
|
||||
active,
|
||||
pending,
|
||||
confirmed,
|
||||
effective,
|
||||
dirty,
|
||||
blockedReason: dirty && !model ? "missing-model" : null,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
|
||||
import { cloneRuntimePermissionState, type RuntimeApprovalPolicy, type RuntimeSandboxPolicy } from "../../../../domain/runtime/permissions";
|
||||
import { type PendingRuntimeIntent, resetRuntimeIntentToConfig, setRuntimeIntentValue, unchangedRuntimeIntent } from "./intent";
|
||||
import { type RuntimeLayeredValue, resolveRuntimeNullablePendingValue, resolveRuntimeValue } from "./layered-value";
|
||||
import type { RuntimeSnapshot } from "./snapshot";
|
||||
|
||||
export interface RuntimePermissionsResolution {
|
||||
readonly permissionProfile: RuntimeLayeredValue<string>;
|
||||
readonly sandboxPolicy: RuntimeLayeredValue<RuntimeSandboxPolicy, RuntimeSandboxPolicy | null>;
|
||||
readonly approvalPolicy: RuntimeLayeredValue<RuntimeApprovalPolicy>;
|
||||
}
|
||||
|
||||
export function resolveRuntimePermissions(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): RuntimePermissionsResolution {
|
||||
return {
|
||||
permissionProfile: resolveRuntimeValue({
|
||||
configured: config.startupPermissions.activePermissionProfile?.id ?? null,
|
||||
active: snapshot.active.activePermissionProfile?.id ?? null,
|
||||
pending: snapshot.pending.permissionProfile,
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.permissionProfileKnown,
|
||||
}),
|
||||
sandboxPolicy: resolveRuntimeSandboxPolicy(snapshot, config),
|
||||
approvalPolicy: resolveRuntimeValue({
|
||||
configured: config.startupPermissions.approvalPolicy,
|
||||
active: snapshot.active.approvalPolicy,
|
||||
pending: snapshot.pending.approvalPolicy,
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.approvalPolicyKnown,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimeSandboxPolicy(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigSnapshot,
|
||||
): RuntimeLayeredValue<RuntimeSandboxPolicy, RuntimeSandboxPolicy | null> {
|
||||
return resolveRuntimeNullablePendingValue({
|
||||
configured: cloneRuntimeSandboxPolicy(config.startupPermissions.sandboxPolicy),
|
||||
active: cloneRuntimeSandboxPolicy(snapshot.active.sandboxPolicy),
|
||||
pending: sandboxPolicyIntentFromPermissionProfile(snapshot.pending.permissionProfile, config),
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.sandboxPolicyKnown,
|
||||
});
|
||||
}
|
||||
|
||||
function sandboxPolicyIntentFromPermissionProfile(
|
||||
intent: PendingRuntimeIntent<string>,
|
||||
config: RuntimeConfigSnapshot,
|
||||
): PendingRuntimeIntent<RuntimeSandboxPolicy | null> {
|
||||
if (intent.kind === "set") return setRuntimeIntentValue(sandboxPolicyForPermissionProfile(intent.value, config));
|
||||
if (intent.kind === "resetToConfig") return resetRuntimeIntentToConfig();
|
||||
return unchangedRuntimeIntent();
|
||||
}
|
||||
|
||||
function sandboxPolicyForPermissionProfile(profile: string, config: RuntimeConfigSnapshot): RuntimeSandboxPolicy | null {
|
||||
return profile === config.startupPermissions.activePermissionProfile?.id
|
||||
? cloneRuntimeSandboxPolicy(config.startupPermissions.sandboxPolicy)
|
||||
: null;
|
||||
}
|
||||
|
||||
function cloneRuntimeSandboxPolicy(value: RuntimeSandboxPolicy | null): RuntimeSandboxPolicy | null {
|
||||
return cloneRuntimePermissionState({ approvalPolicy: null, activePermissionProfile: null, sandboxPolicy: value }).sandboxPolicy;
|
||||
}
|
||||
|
|
@ -1,14 +1,58 @@
|
|||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
|
||||
import type { RuntimeApprovalPolicy, RuntimeSandboxPolicy } from "../../../../domain/runtime/permissions";
|
||||
import { cloneRuntimePermissionState, type RuntimeApprovalPolicy, type RuntimeSandboxPolicy } from "../../../../domain/runtime/permissions";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "../../../../domain/runtime/policy";
|
||||
import { type CollaborationModeResolution, resolveCollaborationMode } from "./collaboration-resolution";
|
||||
import { type RuntimeLayeredValue, resolveRuntimeValue, runtimeLayeredValue } from "./layered-value";
|
||||
import { resolveRuntimePermissions } from "./permission-resolution";
|
||||
import { type AutoReviewResolution, type FastModeResolution, resolveAutoReview, resolveFastMode } from "./runtime-toggles";
|
||||
import {
|
||||
effectiveCollaborationMode,
|
||||
type PendingRuntimeIntent,
|
||||
type RequestedFastMode,
|
||||
resetRuntimeIntentToConfig,
|
||||
setRuntimeIntentValue,
|
||||
unchangedRuntimeIntent,
|
||||
} from "./intent";
|
||||
import {
|
||||
type RuntimeLayeredValue,
|
||||
type RuntimeValueSource,
|
||||
resolveRuntimeNullablePendingValue,
|
||||
resolveRuntimeValue,
|
||||
runtimeLayeredValue,
|
||||
} from "./layered-value";
|
||||
import type { RuntimeSnapshot } from "./snapshot";
|
||||
|
||||
interface AutoReviewResolution {
|
||||
readonly active: boolean;
|
||||
readonly confirmedActive: boolean;
|
||||
readonly source: RuntimeValueSource;
|
||||
readonly confirmedSource: RuntimeValueSource;
|
||||
}
|
||||
|
||||
interface CollaborationModeResolution {
|
||||
readonly active: RuntimeSnapshot["active"]["collaborationMode"];
|
||||
readonly pending: RuntimeSnapshot["pending"]["collaborationMode"];
|
||||
readonly confirmed: NonNullable<RuntimeSnapshot["active"]["collaborationMode"]>;
|
||||
readonly effective: NonNullable<RuntimeSnapshot["active"]["collaborationMode"]>;
|
||||
readonly dirty: boolean;
|
||||
readonly blockedReason: "missing-model" | null;
|
||||
}
|
||||
|
||||
interface FastModeResolution {
|
||||
readonly requested: PendingRuntimeIntent<RequestedFastMode>;
|
||||
readonly active: boolean;
|
||||
readonly confirmedActive: boolean;
|
||||
readonly source: RuntimeValueSource;
|
||||
readonly confirmedSource: RuntimeValueSource;
|
||||
readonly effectiveServiceTier: ServiceTier | null;
|
||||
readonly confirmedServiceTier: ServiceTier | null;
|
||||
readonly serviceTierRequestValue: string;
|
||||
}
|
||||
|
||||
interface RuntimePermissionsResolution {
|
||||
readonly permissionProfile: RuntimeLayeredValue<string>;
|
||||
readonly sandboxPolicy: RuntimeLayeredValue<RuntimeSandboxPolicy, RuntimeSandboxPolicy | null>;
|
||||
readonly approvalPolicy: RuntimeLayeredValue<RuntimeApprovalPolicy>;
|
||||
}
|
||||
|
||||
export interface RuntimeControlsResolution {
|
||||
readonly model: RuntimeLayeredValue<string>;
|
||||
readonly reasoningEffort: RuntimeLayeredValue<ReasoningEffort>;
|
||||
|
|
@ -95,3 +139,105 @@ function serviceTierValue(
|
|||
source,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAutoReview(reviewer: RuntimeLayeredValue<ApprovalsReviewer>): AutoReviewResolution {
|
||||
return {
|
||||
active: autoReviewActive(reviewer.effective),
|
||||
confirmedActive: autoReviewActive(reviewer.confirmed),
|
||||
source: reviewer.source,
|
||||
confirmedSource: reviewer.confirmedSource,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFastMode(
|
||||
requested: PendingRuntimeIntent<RequestedFastMode>,
|
||||
serviceTier: RuntimeLayeredValue<ServiceTier>,
|
||||
serviceTiers: ModelMetadata["serviceTiers"],
|
||||
): FastModeResolution {
|
||||
return {
|
||||
requested,
|
||||
active: isFastServiceTier(serviceTier.effective, serviceTiers),
|
||||
confirmedActive: isFastServiceTier(serviceTier.confirmed, serviceTiers),
|
||||
source: serviceTier.source,
|
||||
confirmedSource: serviceTier.confirmedSource,
|
||||
effectiveServiceTier: serviceTier.effective,
|
||||
confirmedServiceTier: serviceTier.confirmed,
|
||||
serviceTierRequestValue: serviceTiers.find((tier) => tier.name.trim().toLowerCase() === "fast")?.id ?? "fast",
|
||||
};
|
||||
}
|
||||
|
||||
function autoReviewActive(value: ApprovalsReviewer | null): boolean {
|
||||
return value === "auto_review" || value === "guardian_subagent";
|
||||
}
|
||||
|
||||
function isFastServiceTier(value: string | null | undefined, serviceTiers: ModelMetadata["serviceTiers"]): boolean {
|
||||
if (!value) return false;
|
||||
if (value === "fast") return true;
|
||||
if (serviceTiers.length === 0) return value === "priority";
|
||||
return serviceTiers.some((tier) => tier.id === value && tier.name.trim().toLowerCase() === "fast");
|
||||
}
|
||||
|
||||
function resolveCollaborationMode(snapshot: RuntimeSnapshot, model: string | null): CollaborationModeResolution {
|
||||
const active = snapshot.active.collaborationMode;
|
||||
const pending = snapshot.pending.collaborationMode;
|
||||
const confirmed = effectiveCollaborationMode(active);
|
||||
const effective = pending.kind === "set" ? pending.value : confirmed;
|
||||
const dirty = pending.kind === "set";
|
||||
return {
|
||||
active,
|
||||
pending,
|
||||
confirmed,
|
||||
effective,
|
||||
dirty,
|
||||
blockedReason: dirty && !model ? "missing-model" : null,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimePermissions(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): RuntimePermissionsResolution {
|
||||
return {
|
||||
permissionProfile: resolveRuntimeValue({
|
||||
configured: config.startupPermissions.activePermissionProfile?.id ?? null,
|
||||
active: snapshot.active.activePermissionProfile?.id ?? null,
|
||||
pending: snapshot.pending.permissionProfile,
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.permissionProfileKnown,
|
||||
}),
|
||||
sandboxPolicy: resolveRuntimeSandboxPolicy(snapshot, config),
|
||||
approvalPolicy: resolveRuntimeValue({
|
||||
configured: config.startupPermissions.approvalPolicy,
|
||||
active: snapshot.active.approvalPolicy,
|
||||
pending: snapshot.pending.approvalPolicy,
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.approvalPolicyKnown,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRuntimeSandboxPolicy(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigSnapshot,
|
||||
): RuntimeLayeredValue<RuntimeSandboxPolicy, RuntimeSandboxPolicy | null> {
|
||||
return resolveRuntimeNullablePendingValue({
|
||||
configured: cloneRuntimeSandboxPolicy(config.startupPermissions.sandboxPolicy),
|
||||
active: cloneRuntimeSandboxPolicy(snapshot.active.sandboxPolicy),
|
||||
pending: sandboxPolicyIntentFromPermissionProfile(snapshot.pending.permissionProfile, config),
|
||||
activeKnown: snapshot.activeThreadId !== null && snapshot.active.sandboxPolicyKnown,
|
||||
});
|
||||
}
|
||||
|
||||
function sandboxPolicyIntentFromPermissionProfile(
|
||||
intent: PendingRuntimeIntent<string>,
|
||||
config: RuntimeConfigSnapshot,
|
||||
): PendingRuntimeIntent<RuntimeSandboxPolicy | null> {
|
||||
if (intent.kind === "set") return setRuntimeIntentValue(sandboxPolicyForPermissionProfile(intent.value, config));
|
||||
if (intent.kind === "resetToConfig") return resetRuntimeIntentToConfig();
|
||||
return unchangedRuntimeIntent();
|
||||
}
|
||||
|
||||
function sandboxPolicyForPermissionProfile(profile: string, config: RuntimeConfigSnapshot): RuntimeSandboxPolicy | null {
|
||||
return profile === config.startupPermissions.activePermissionProfile?.id
|
||||
? cloneRuntimeSandboxPolicy(config.startupPermissions.sandboxPolicy)
|
||||
: null;
|
||||
}
|
||||
|
||||
function cloneRuntimeSandboxPolicy(value: RuntimeSandboxPolicy | null): RuntimeSandboxPolicy | null {
|
||||
return cloneRuntimePermissionState({ approvalPolicy: null, activePermissionProfile: null, sandboxPolicy: value }).sandboxPolicy;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
import type { ModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import type { ApprovalsReviewer, ServiceTier } from "../../../../domain/runtime/policy";
|
||||
import type { PendingRuntimeIntent, RequestedFastMode } from "./intent";
|
||||
import type { RuntimeLayeredValue, RuntimeValueSource } from "./layered-value";
|
||||
|
||||
export interface FastModeResolution {
|
||||
readonly requested: PendingRuntimeIntent<RequestedFastMode>;
|
||||
readonly active: boolean;
|
||||
readonly confirmedActive: boolean;
|
||||
readonly source: RuntimeValueSource;
|
||||
readonly confirmedSource: RuntimeValueSource;
|
||||
readonly effectiveServiceTier: ServiceTier | null;
|
||||
readonly confirmedServiceTier: ServiceTier | null;
|
||||
readonly serviceTierRequestValue: string;
|
||||
}
|
||||
|
||||
export interface AutoReviewResolution {
|
||||
readonly active: boolean;
|
||||
readonly confirmedActive: boolean;
|
||||
readonly source: RuntimeValueSource;
|
||||
readonly confirmedSource: RuntimeValueSource;
|
||||
}
|
||||
|
||||
export function resolveAutoReview(reviewer: RuntimeLayeredValue<ApprovalsReviewer>): AutoReviewResolution {
|
||||
return {
|
||||
active: autoReviewActive(reviewer.effective),
|
||||
confirmedActive: autoReviewActive(reviewer.confirmed),
|
||||
source: reviewer.source,
|
||||
confirmedSource: reviewer.confirmedSource,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveFastMode(
|
||||
requested: PendingRuntimeIntent<RequestedFastMode>,
|
||||
serviceTier: RuntimeLayeredValue<ServiceTier>,
|
||||
serviceTiers: ModelMetadata["serviceTiers"],
|
||||
): FastModeResolution {
|
||||
return {
|
||||
requested,
|
||||
active: isFastServiceTier(serviceTier.effective, serviceTiers),
|
||||
confirmedActive: isFastServiceTier(serviceTier.confirmed, serviceTiers),
|
||||
source: serviceTier.source,
|
||||
confirmedSource: serviceTier.confirmedSource,
|
||||
effectiveServiceTier: serviceTier.effective,
|
||||
confirmedServiceTier: serviceTier.confirmed,
|
||||
serviceTierRequestValue: serviceTiers.find((tier) => tier.name.trim().toLowerCase() === "fast")?.id ?? "fast",
|
||||
};
|
||||
}
|
||||
|
||||
function autoReviewActive(value: ApprovalsReviewer | null): boolean {
|
||||
return value === "auto_review" || value === "guardian_subagent";
|
||||
}
|
||||
|
||||
function isFastServiceTier(value: string | null | undefined, serviceTiers: ModelMetadata["serviceTiers"]): boolean {
|
||||
if (!value) return false;
|
||||
if (value === "fast") return true;
|
||||
if (serviceTiers.length === 0) return value === "priority";
|
||||
return serviceTiers.some((tier) => tier.id === value && tier.name.trim().toLowerCase() === "fast");
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import type { ConnectionManager } from "../../../../app-server/connection/connection-manager";
|
||||
import type { ConnectionWorkTracker } from "../../application/connection/connection-work";
|
||||
import { type ChatReconnectActionsHost, reconnectPanel } from "../../application/connection/reconnect-actions";
|
||||
import type { ChatConnectionPhase } from "../../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { ChatViewDeferredTasks } from "../session/deferred-work";
|
||||
|
||||
interface ChatPanelReconnectStatus {
|
||||
set: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
interface ChatPanelReconnectHost {
|
||||
stateStore: ChatStateStore;
|
||||
connectionWork: ConnectionWorkTracker;
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
}
|
||||
|
||||
interface ChatPanelReconnectInput {
|
||||
connection: ConnectionManager;
|
||||
ensureConnected: () => Promise<void>;
|
||||
invalidateThreadWork: () => void;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
status: ChatPanelReconnectStatus;
|
||||
}
|
||||
|
||||
export function createReconnectAction(host: ChatPanelReconnectHost, input: ChatPanelReconnectInput): () => Promise<void> {
|
||||
const reconnectHost: ChatReconnectActionsHost = {
|
||||
stateStore: host.stateStore,
|
||||
invalidateConnectionWork: () => {
|
||||
host.connectionWork.invalidate();
|
||||
},
|
||||
invalidateThreadWork: () => {
|
||||
input.invalidateThreadWork();
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
host.deferredTasks.clearDiagnostics();
|
||||
},
|
||||
resetConnection: () => {
|
||||
input.connection.resetConnection();
|
||||
},
|
||||
setStatus: (statusText, phase) => {
|
||||
input.status.set(statusText, phase);
|
||||
},
|
||||
ensureConnected: input.ensureConnected,
|
||||
resumeThread: input.resumeThread,
|
||||
addSystemMessage: (text) => {
|
||||
input.status.addSystemMessage(text);
|
||||
},
|
||||
};
|
||||
|
||||
return () => reconnectPanel(reconnectHost);
|
||||
}
|
||||
|
|
@ -9,18 +9,17 @@ import type { ChatAppServerGateway } from "../../app-server/session-gateway";
|
|||
import type { LocalIdSource } from "../../application/local-id-source";
|
||||
import { messageStreamItems } from "../../application/state/message-stream";
|
||||
import type { ChatStateStore } from "../../application/state/store";
|
||||
import type { ActiveThreadIdentitySync } from "../../application/threads/active-thread-identity-sync";
|
||||
import { type ActiveThreadIdentitySync, createActiveThreadIdentitySync } from "../../application/threads/active-thread-identity-sync";
|
||||
import { type AutoTitleCoordinator, createAutoTitleCoordinator } from "../../application/threads/auto-title-coordinator";
|
||||
import { createGoalActions, createThreadGoalSyncActions } from "../../application/threads/goal-actions";
|
||||
import { HistoryController } from "../../application/threads/history-controller";
|
||||
import { createThreadLifecycleParts } from "../../application/threads/lifecycle-parts";
|
||||
import {
|
||||
activeThreadRenameTitleContext,
|
||||
createThreadRenameEditorActions,
|
||||
type ThreadRenameEditorActions,
|
||||
} from "../../application/threads/rename-editor-actions";
|
||||
import type { RestorationController } from "../../application/threads/restoration-controller";
|
||||
import type { ResumeActions } from "../../application/threads/resume-actions";
|
||||
import { RestorationController } from "../../application/threads/restoration-controller";
|
||||
import { createResumeActions, type ResumeActions } from "../../application/threads/resume-actions";
|
||||
import type { ChatResumeWorkTracker } from "../../application/threads/resume-work";
|
||||
import { createThreadManagementActions, type ThreadManagementActionsHost } from "../../application/threads/thread-management-actions";
|
||||
import { createThreadNavigationActions } from "../../application/threads/thread-navigation-actions";
|
||||
|
|
@ -31,10 +30,16 @@ import type { ChatPanelEnvironment } from "../contracts";
|
|||
|
||||
type ChatPanelGoalSyncActions = ReturnType<typeof createThreadGoalSyncActions>;
|
||||
export type ChatPanelGoalActions = ReturnType<typeof createGoalActions>;
|
||||
export type ChatPanelThreadLifecycle = ReturnType<typeof createThreadLifecycleParts>;
|
||||
export type ChatPanelThreadActions = ReturnType<typeof createThreadManagementActions>;
|
||||
export type ChatPanelThreadNavigationActions = ReturnType<typeof createThreadNavigationActions>;
|
||||
|
||||
export interface ChatPanelThreadLifecycle {
|
||||
history: HistoryController;
|
||||
restoration: RestorationController;
|
||||
resume: ResumeActions;
|
||||
identity: ActiveThreadIdentitySync;
|
||||
}
|
||||
|
||||
interface ChatPanelThreadStatus {
|
||||
set: (statusText: string) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
|
|
@ -378,28 +383,42 @@ function createSessionThreadLifecycle(
|
|||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
} = input;
|
||||
return createThreadLifecycleParts({
|
||||
const restoration = new RestorationController({
|
||||
invalidateThreadWork,
|
||||
setStatus: status.set,
|
||||
refreshTabHeader,
|
||||
});
|
||||
const resetThreadTurnPresence = (hadTurns: boolean) => {
|
||||
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
|
||||
};
|
||||
const resume = createResumeActions({
|
||||
stateStore: host.stateStore,
|
||||
resumeTransport: appServer.threadResume,
|
||||
lifecycle: {
|
||||
resumeWork: host.resumeWork,
|
||||
history,
|
||||
invalidateThreadWork,
|
||||
getClosing: host.getClosing,
|
||||
recoverTokenUsageFromRollout: (path) =>
|
||||
recoverRolloutTokenUsage(path, (filePath, options) => appServer.readFileBase64(filePath, options)),
|
||||
},
|
||||
thread: {
|
||||
notifyIdentityChanged: notifyActiveThreadIdentityChanged,
|
||||
refreshTabHeader,
|
||||
},
|
||||
status,
|
||||
liveState: {
|
||||
refresh: refreshLiveState,
|
||||
},
|
||||
goals,
|
||||
resetThreadTurnPresence: (hadTurns) => {
|
||||
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
resumeWork: host.resumeWork,
|
||||
history,
|
||||
restoration,
|
||||
closing: host.getClosing,
|
||||
resetThreadTurnPresence,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
refreshLiveState,
|
||||
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
|
||||
recoverTokenUsageFromRollout: (path) =>
|
||||
recoverRolloutTokenUsage(path, (filePath, options) => appServer.readFileBase64(filePath, options)),
|
||||
});
|
||||
const identity = createActiveThreadIdentitySync({
|
||||
stateStore: host.stateStore,
|
||||
restoration,
|
||||
invalidateThreadWork,
|
||||
resetThreadTurnPresence,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
refreshTabHeader,
|
||||
});
|
||||
|
||||
return {
|
||||
history,
|
||||
restoration,
|
||||
resume,
|
||||
identity,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ConnectionManager } from "../../../app-server/connection/connection-man
|
|||
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
|
||||
import { createChatAppServerGateway } from "../app-server/session-gateway";
|
||||
import type { ConnectionWorkTracker } from "../application/connection/connection-work";
|
||||
import { reconnectPanel } from "../application/connection/reconnect-actions";
|
||||
import { createLocalIdSource, type LocalIdSource } from "../application/local-id-source";
|
||||
import type { ChatAction, ChatConnectionPhase } from "../application/state/root-reducer";
|
||||
import type { ChatStateStore } from "../application/state/store";
|
||||
|
|
@ -15,7 +16,6 @@ import type { ChatComposerController } from "../panel/composer-controller";
|
|||
import type { ChatMessageStreamScrollBinding } from "../panel/message-stream-scroll-binding";
|
||||
import { createChatComposerController } from "./bundles/composer-bundle";
|
||||
import { type ChatPanelConnectionBundle, createConnectionBundle } from "./bundles/connection-bundle";
|
||||
import { createReconnectAction } from "./bundles/reconnect-bundle";
|
||||
import { createRuntimeBundle } from "./bundles/runtime-bundle";
|
||||
import { type ChatPanelShellBundle, createShellBundle } from "./bundles/shell-bundle";
|
||||
import { createThreadActionBundle, createThreadFoundation, createThreadLifecycleBundle } from "./bundles/thread-bundle";
|
||||
|
|
@ -164,15 +164,30 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
refreshActiveThreads,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
});
|
||||
const reconnect = createReconnectAction(host, {
|
||||
connection,
|
||||
ensureConnected,
|
||||
invalidateThreadWork: () => {
|
||||
threadFoundation.invalidateThreadWork();
|
||||
},
|
||||
resumeThread: (threadId) => threadLifecycle.resume.resumeThread(threadId),
|
||||
status,
|
||||
});
|
||||
const reconnect = () =>
|
||||
reconnectPanel({
|
||||
stateStore,
|
||||
invalidateConnectionWork: () => {
|
||||
host.connectionWork.invalidate();
|
||||
},
|
||||
invalidateThreadWork: () => {
|
||||
threadFoundation.invalidateThreadWork();
|
||||
},
|
||||
clearDeferredDiagnostics: () => {
|
||||
host.deferredTasks.clearDiagnostics();
|
||||
},
|
||||
resetConnection: () => {
|
||||
connection.resetConnection();
|
||||
},
|
||||
setStatus: (statusText, phase) => {
|
||||
status.set(statusText, phase);
|
||||
},
|
||||
ensureConnected,
|
||||
resumeThread: (threadId) => threadLifecycle.resume.resumeThread(threadId),
|
||||
addSystemMessage: (text) => {
|
||||
status.addSystemMessage(text);
|
||||
},
|
||||
});
|
||||
const turn = createTurnBundle(host, {
|
||||
localItemIds,
|
||||
appServer,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { ObservedResult } from "../../../../app-server/query/observed-result";
|
||||
import { observedValue } from "../../../../app-server/query/observed-result";
|
||||
import type { ModelMetadata } from "../../../../domain/catalog/metadata";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
|
|
@ -45,21 +44,21 @@ export function createChatPanelSharedStateBinding(options: ChatPanelSharedStateB
|
|||
refreshTabHeader();
|
||||
};
|
||||
const receiveThreadResult = (result: ObservedResult<readonly Thread[]>): void => {
|
||||
const observedThreads = observedValue(result);
|
||||
const observedThreads = result.value;
|
||||
if (observedThreads) receiveThreads(observedThreads);
|
||||
};
|
||||
const receiveAppServerMetadata = (metadata: SharedServerMetadata): void => {
|
||||
serverActions.metadata.applyAppServerMetadata(metadata);
|
||||
};
|
||||
const receiveAppServerMetadataResult = (result: ObservedResult<SharedServerMetadata>): void => {
|
||||
const observedMetadata = observedValue(result);
|
||||
const observedMetadata = result.value;
|
||||
if (observedMetadata) receiveAppServerMetadata(observedMetadata);
|
||||
};
|
||||
const receiveModels = (models: readonly ModelMetadata[]): void => {
|
||||
stateStore.dispatch({ type: "connection/metadata-applied", availableModels: models });
|
||||
};
|
||||
const receiveModelsResult = (result: ObservedResult<readonly ModelMetadata[]>): void => {
|
||||
const observedModels = observedValue(result);
|
||||
const observedModels = result.value;
|
||||
if (observedModels) receiveModels(observedModels);
|
||||
};
|
||||
const unsubscribe = (): void => {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ import type { MessageStreamScrollPortBinding } from "../../ui/message-stream/flo
|
|||
import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer.obsidian";
|
||||
import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks";
|
||||
import type { ChatPanelMessageStreamReadModel } from "../shell-read-model";
|
||||
import {
|
||||
type ChatMessageStreamSurfaceContext,
|
||||
createMessageStreamSurfaceContext,
|
||||
messageStreamSurfaceProjectionFromModel,
|
||||
} from "./message-stream-projection";
|
||||
import { type ChatMessageStreamSurfaceContext, messageStreamSurfaceProjectionFromModel } from "./message-stream-projection";
|
||||
|
||||
export interface ChatPanelMessageStreamPresenter {
|
||||
renderState(model: ChatPanelMessageStreamReadModel): MessageStreamViewportState;
|
||||
|
|
@ -79,6 +75,7 @@ export interface MessageStreamPresenterOptions {
|
|||
|
||||
export class MessageStreamPresenter {
|
||||
private readonly obsidianMarkdownRenderer: MarkdownMessageRenderer;
|
||||
private readonly surfaceContext: ChatMessageStreamSurfaceContext;
|
||||
|
||||
constructor(private readonly options: MessageStreamPresenterOptions) {
|
||||
this.obsidianMarkdownRenderer = new MarkdownMessageRenderer({
|
||||
|
|
@ -86,18 +83,34 @@ export class MessageStreamPresenter {
|
|||
owner: options.obsidian.owner,
|
||||
vaultPath: options.workspace.vaultPath,
|
||||
});
|
||||
}
|
||||
|
||||
private dispatch(action: ChatAction): void {
|
||||
this.options.state.store.dispatch(action);
|
||||
this.surfaceContext = {
|
||||
vaultPath: options.workspace.vaultPath,
|
||||
setDisclosureOpen: (bucket, id, open) => {
|
||||
this.dispatch({ type: "ui/disclosure-set", bucket, id, open });
|
||||
},
|
||||
setForkMenuItem: (itemId) => {
|
||||
this.dispatch({ type: "ui/message-fork-menu-set", itemId });
|
||||
},
|
||||
loadOlderTurns: () => {
|
||||
options.history.loadOlderTurns();
|
||||
},
|
||||
renderObsidianMarkdown: (element, text) => {
|
||||
this.obsidianMarkdownRenderer.renderObsidianMarkdown(element, text);
|
||||
},
|
||||
renderStreamMarkdown: (element, text) => {
|
||||
renderStreamMarkdown(element, text, {
|
||||
app: options.obsidian.app,
|
||||
vaultPath: options.workspace.vaultPath,
|
||||
});
|
||||
},
|
||||
copyMessageText: (text) => void this.copyMessageText(text),
|
||||
actions: options.actions,
|
||||
requests: options.requests,
|
||||
};
|
||||
}
|
||||
|
||||
renderState(model: ChatPanelMessageStreamReadModel): MessageStreamViewportState {
|
||||
return this.renderStateFor(model);
|
||||
}
|
||||
|
||||
private renderStateFor(model: ChatPanelMessageStreamReadModel): MessageStreamViewportState {
|
||||
const projection = messageStreamSurfaceProjectionFromModel(model, this.messageStreamSurfaceContext());
|
||||
const projection = messageStreamSurfaceProjectionFromModel(model, this.surfaceContext);
|
||||
|
||||
return {
|
||||
blocks: projection.blocks,
|
||||
|
|
@ -106,28 +119,8 @@ export class MessageStreamPresenter {
|
|||
};
|
||||
}
|
||||
|
||||
private messageStreamSurfaceContext(): ChatMessageStreamSurfaceContext {
|
||||
return createMessageStreamSurfaceContext({
|
||||
vaultPath: this.options.workspace.vaultPath,
|
||||
dispatch: (action) => {
|
||||
this.dispatch(action);
|
||||
},
|
||||
loadOlderTurns: () => {
|
||||
this.options.history.loadOlderTurns();
|
||||
},
|
||||
renderObsidianMarkdown: (element, text) => {
|
||||
this.obsidianMarkdownRenderer.renderObsidianMarkdown(element, text);
|
||||
},
|
||||
renderStreamMarkdown: (element, text) => {
|
||||
renderStreamMarkdown(element, text, {
|
||||
app: this.options.obsidian.app,
|
||||
vaultPath: this.options.workspace.vaultPath,
|
||||
});
|
||||
},
|
||||
copyMessageText: (text) => void this.copyMessageText(text),
|
||||
actions: this.options.actions,
|
||||
requests: this.options.requests,
|
||||
});
|
||||
private dispatch(action: ChatAction): void {
|
||||
this.options.state.store.dispatch(action);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import type { TurnDiffViewState } from "../../../turn-diff/model";
|
||||
import type { PendingRequestBlockActions } from "../../application/pending-requests/block";
|
||||
import { type PendingRequestBlockActions, pendingRequestBlockStateFromRequestState } from "../../application/pending-requests/block";
|
||||
import type { ChatRequestState } from "../../application/pending-requests/state";
|
||||
import type { MessageStreamRollbackCandidate } from "../../application/state/message-stream";
|
||||
import type { ChatAction } from "../../application/state/root-reducer";
|
||||
import { type ForkCandidate, messageStreamSegmentsEmpty, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
import { pendingRequestsSignature } from "../../domain/pending-requests/signatures";
|
||||
import type { MessageStreamTextActionTargets } from "../../presentation/message-stream/text-view";
|
||||
import { type MessageStreamViewBlock, messageStreamViewBlocks } from "../../presentation/message-stream/view-model";
|
||||
import type { PendingRequestBlockSnapshot } from "../../presentation/pending-requests/view-model";
|
||||
import { type PendingRequestBlockSnapshot, pendingRequestBlockSnapshotFromState } from "../../presentation/pending-requests/view-model";
|
||||
import type { MessageStreamContext, MessageStreamDisclosureBucket, MessageStreamDisclosureState } from "../../ui/message-stream/context";
|
||||
import type { ChatPanelMessageStreamReadModel } from "../shell-read-model";
|
||||
import { pendingRequestSurfaceProjectionFromState } from "./pending-request-block-projection";
|
||||
|
||||
interface ChatMessageStreamActions {
|
||||
rollbackThread: (threadId: string) => void;
|
||||
|
|
@ -34,17 +34,6 @@ export interface ChatMessageStreamSurfaceContext {
|
|||
requests: ChatMessageStreamRequests;
|
||||
}
|
||||
|
||||
export interface MessageStreamSurfaceContextOptions {
|
||||
vaultPath: string;
|
||||
dispatch: (action: ChatAction) => void;
|
||||
loadOlderTurns: () => void;
|
||||
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
|
||||
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
|
||||
copyMessageText: (text: string) => void;
|
||||
actions: ChatMessageStreamActions;
|
||||
requests: ChatMessageStreamRequests;
|
||||
}
|
||||
|
||||
interface MessageStreamStateProjection {
|
||||
activeThreadId: string | null;
|
||||
workspaceRoot: string;
|
||||
|
|
@ -54,29 +43,16 @@ interface MessageStreamStateProjection {
|
|||
viewBlocks: readonly MessageStreamViewBlock[];
|
||||
}
|
||||
|
||||
interface PendingRequestSurfaceProjection {
|
||||
readonly signature: string;
|
||||
readonly snapshot: PendingRequestBlockSnapshot;
|
||||
}
|
||||
|
||||
export interface MessageStreamSurfaceProjection {
|
||||
blocks: readonly MessageStreamViewBlock[];
|
||||
context: MessageStreamContext;
|
||||
}
|
||||
|
||||
export function createMessageStreamSurfaceContext(options: MessageStreamSurfaceContextOptions): ChatMessageStreamSurfaceContext {
|
||||
return {
|
||||
vaultPath: options.vaultPath,
|
||||
setDisclosureOpen: (bucket, id, open) => {
|
||||
options.dispatch({ type: "ui/disclosure-set", bucket, id, open });
|
||||
},
|
||||
setForkMenuItem: (itemId) => {
|
||||
options.dispatch({ type: "ui/message-fork-menu-set", itemId });
|
||||
},
|
||||
loadOlderTurns: options.loadOlderTurns,
|
||||
renderObsidianMarkdown: options.renderObsidianMarkdown,
|
||||
renderStreamMarkdown: options.renderStreamMarkdown,
|
||||
copyMessageText: options.copyMessageText,
|
||||
actions: options.actions,
|
||||
requests: options.requests,
|
||||
};
|
||||
}
|
||||
|
||||
export function messageStreamSurfaceProjectionFromModel(
|
||||
model: ChatPanelMessageStreamReadModel,
|
||||
context: ChatMessageStreamSurfaceContext,
|
||||
|
|
@ -188,6 +164,24 @@ function textActionTargetsForMessageStreamItems(
|
|||
return byItemId;
|
||||
}
|
||||
|
||||
function pendingRequestSurfaceProjectionFromState(
|
||||
requests: ChatRequestState,
|
||||
approvalDetails: ReadonlySet<string>,
|
||||
): PendingRequestSurfaceProjection | null {
|
||||
const signature = pendingRequestsSignature(
|
||||
requests.approvals,
|
||||
requests.pendingUserInputs,
|
||||
requests.pendingMcpElicitations,
|
||||
requests.userInputDrafts,
|
||||
requests.mcpElicitationDrafts,
|
||||
);
|
||||
if (!signature) return null;
|
||||
return {
|
||||
signature,
|
||||
snapshot: pendingRequestBlockSnapshotFromState(pendingRequestBlockStateFromRequestState(requests, approvalDetails)),
|
||||
};
|
||||
}
|
||||
|
||||
function patchTextActionTargets(
|
||||
byItemId: Map<string, MessageStreamTextActionTargets>,
|
||||
itemId: string,
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { pendingRequestBlockStateFromRequestState } from "../../application/pending-requests/block";
|
||||
import type { ChatRequestState } from "../../application/pending-requests/state";
|
||||
import { pendingRequestsSignature } from "../../domain/pending-requests/signatures";
|
||||
import { type PendingRequestBlockSnapshot, pendingRequestBlockSnapshotFromState } from "../../presentation/pending-requests/view-model";
|
||||
|
||||
export interface PendingRequestSurfaceProjection {
|
||||
readonly signature: string;
|
||||
readonly snapshot: PendingRequestBlockSnapshot;
|
||||
}
|
||||
|
||||
export function pendingRequestSurfaceProjectionFromState(
|
||||
requests: ChatRequestState,
|
||||
approvalDetails: ReadonlySet<string>,
|
||||
): PendingRequestSurfaceProjection | null {
|
||||
const signature = pendingRequestsSignature(
|
||||
requests.approvals,
|
||||
requests.pendingUserInputs,
|
||||
requests.pendingMcpElicitations,
|
||||
requests.userInputDrafts,
|
||||
requests.mcpElicitationDrafts,
|
||||
);
|
||||
if (!signature) return null;
|
||||
return {
|
||||
signature,
|
||||
snapshot: pendingRequestBlockSnapshotFromState(pendingRequestBlockStateFromRequestState(requests, approvalDetails)),
|
||||
};
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Notice } from "obsidian";
|
|||
|
||||
import type { AppServerClientAccess } from "../../app-server/connection/client-access";
|
||||
import type { ObservedResult } from "../../app-server/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading, observedValue } from "../../app-server/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading } from "../../app-server/query/observed-result";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../app-server/query/shared-queries";
|
||||
import type { ReasoningEffort } from "../../domain/catalog/metadata";
|
||||
import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown";
|
||||
|
|
@ -161,7 +161,7 @@ export class ThreadsViewSession {
|
|||
}
|
||||
|
||||
private receiveObservedThreadsResult(result: ObservedResult<readonly Thread[]>): void {
|
||||
const observedThreads = observedValue(result);
|
||||
const observedThreads = result.value;
|
||||
if (observedThreads) {
|
||||
this.receiveObservedThreads(observedThreads);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import type { ObservedResult } from "../app-server/query/observed-result";
|
||||
import { observedValue } from "../app-server/query/observed-result";
|
||||
import type { HookItem, ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata";
|
||||
import { findModelMetadataByIdOrName, sortedModelMetadata, supportedEffortsForModelMetadata } from "../domain/catalog/metadata";
|
||||
import type { Thread } from "../domain/threads/model";
|
||||
import { threadArchiveDisplayTitle } from "../domain/threads/title";
|
||||
import { isStaleSettingsDynamicDataContextError } from "./dynamic-data";
|
||||
import type { SettingsDynamicSectionsHost } from "./host";
|
||||
import {
|
||||
createSettingsDynamicSectionLifecycle,
|
||||
type SettingsDynamicSectionLifecycleState,
|
||||
settingsDynamicSectionFailed,
|
||||
settingsDynamicSectionLoaded,
|
||||
settingsDynamicSectionLoading,
|
||||
} from "./lifecycle";
|
||||
|
||||
interface SettingsDynamicSectionsControllerCallbacks {
|
||||
display(): void;
|
||||
notify(message: string): void;
|
||||
}
|
||||
|
||||
type SettingsDynamicSectionLifecycleState =
|
||||
| { kind: "idle"; status: "" }
|
||||
| { kind: "loading"; status: string }
|
||||
| { kind: "loaded"; status: string }
|
||||
| { kind: "failed"; status: string };
|
||||
|
||||
export interface SettingsDynamicSectionsSnapshot {
|
||||
archivedThreads: readonly Thread[];
|
||||
archivedThreadsLifecycle: SettingsDynamicSectionLifecycleState;
|
||||
|
|
@ -110,14 +108,14 @@ export class SettingsDynamicSectionsController {
|
|||
}
|
||||
|
||||
private receiveObservedModelsResult(result: ObservedResult<readonly ModelMetadata[]>): void {
|
||||
const observedModels = observedValue(result);
|
||||
const observedModels = result.value;
|
||||
if (!observedModels) return;
|
||||
this.models = [...observedModels];
|
||||
this.callbacks.display();
|
||||
}
|
||||
|
||||
private receiveObservedArchivedThreadsResult(result: ObservedResult<readonly Thread[]>): void {
|
||||
const observedThreads = observedValue(result);
|
||||
const observedThreads = result.value;
|
||||
if (!observedThreads) return;
|
||||
this.archivedThreads = [...observedThreads];
|
||||
this.archivedThreadsLoaded = true;
|
||||
|
|
@ -391,6 +389,22 @@ function archivedThreadsStatus(count: number): string {
|
|||
return `Loaded ${String(count)} archived thread${count === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
||||
function createSettingsDynamicSectionLifecycle(): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "idle", status: "" };
|
||||
}
|
||||
|
||||
function settingsDynamicSectionLoading(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "loading", status };
|
||||
}
|
||||
|
||||
function settingsDynamicSectionLoaded(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "loaded", status };
|
||||
}
|
||||
|
||||
function settingsDynamicSectionFailed(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "failed", status };
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
export type SettingsDynamicSectionLifecycleState =
|
||||
| { kind: "idle"; status: "" }
|
||||
| { kind: "loading"; status: string }
|
||||
| { kind: "loaded"; status: string }
|
||||
| { kind: "failed"; status: string };
|
||||
|
||||
export function createSettingsDynamicSectionLifecycle(): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "idle", status: "" };
|
||||
}
|
||||
|
||||
export function settingsDynamicSectionLoading(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "loading", status };
|
||||
}
|
||||
|
||||
export function settingsDynamicSectionLoaded(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "loaded", status };
|
||||
}
|
||||
|
||||
export function settingsDynamicSectionFailed(status: string): SettingsDynamicSectionLifecycleState {
|
||||
return { kind: "failed", status };
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ObservedResult } from "../../../src/app-server/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading, observedValue } from "../../../src/app-server/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading } from "../../../src/app-server/query/observed-result";
|
||||
|
||||
describe("observed query result helpers", () => {
|
||||
it("treats successful empty arrays as current values", () => {
|
||||
|
|
@ -19,11 +19,6 @@ describe("observed query result helpers", () => {
|
|||
expect(observedInitialLoading(observedResult({ value: null, isFetching: true }), null)).toBe(true);
|
||||
expect(observedInitialError(observedResult({ value: null, error }), null)).toBe(error);
|
||||
});
|
||||
|
||||
it("projects nullable observed values without reinterpreting empty values", () => {
|
||||
expect(observedValue(observedResult({ value: [] as readonly string[] }))).toEqual([]);
|
||||
expect(observedValue(observedResult({ value: null }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function observedResult<T>(overrides: Partial<ObservedResult<T>> & Pick<ObservedResult<T>, "value">): ObservedResult<T> {
|
||||
|
|
|
|||
|
|
@ -4,18 +4,8 @@ import type { AppServerClient, ClientResponseByMethod } from "../../../../src/ap
|
|||
import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
|
||||
import type { TurnItem, TurnRecord } from "../../../../src/app-server/protocol/turn";
|
||||
import type { CodexInput } from "../../../../src/domain/chat/input";
|
||||
import { createChatAppServerGateway } from "../../../../src/features/chat/app-server/session-gateway";
|
||||
import { createThreadReferenceResolver } from "../../../../src/features/chat/app-server/thread-reference-resolver";
|
||||
import {
|
||||
createChatThreadGoalReadTransport,
|
||||
createChatThreadGoalTransport,
|
||||
} from "../../../../src/features/chat/app-server/transports/goal-transport";
|
||||
import {
|
||||
createChatThreadHistoryTransport,
|
||||
createChatThreadResumeTransport,
|
||||
} from "../../../../src/features/chat/app-server/transports/thread-loading-transport";
|
||||
import { createChatThreadMutationTransport } from "../../../../src/features/chat/app-server/transports/thread-mutation-transport";
|
||||
import { createChatRuntimeSettingsTransport } from "../../../../src/features/chat/app-server/transports/thread-settings-transport";
|
||||
import { createChatTurnTransport } from "../../../../src/features/chat/app-server/transports/turn-transport";
|
||||
import { deferred } from "../../../support/async";
|
||||
|
||||
const textInput = (text: string): CodexInput => [{ type: "text", text }];
|
||||
|
|
@ -24,11 +14,10 @@ describe("chat app-server transports", () => {
|
|||
it("starts turns with the session vault path and returns chat-owned turn ids", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ turn: { id: "turn-1" } });
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatTurnTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
}).turn;
|
||||
|
||||
await expect(
|
||||
transport.startTurn({
|
||||
|
|
@ -51,11 +40,10 @@ describe("chat app-server transports", () => {
|
|||
const firstClient = { request: vi.fn().mockReturnValue(start.promise) } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createChatTurnTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
});
|
||||
}).turn;
|
||||
|
||||
const starting = transport.startTurn({ threadId: "thread", input: textInput("hello"), clientUserMessageId: "local-user" });
|
||||
currentClient = secondClient;
|
||||
|
|
@ -67,11 +55,10 @@ describe("chat app-server transports", () => {
|
|||
it("compacts threads through a connected app-server client", async () => {
|
||||
const request = vi.fn().mockResolvedValue({});
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatThreadMutationTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
}).threadMutation;
|
||||
|
||||
await expect(transport.compactThread("thread")).resolves.toBe(true);
|
||||
|
||||
|
|
@ -81,11 +68,10 @@ describe("chat app-server transports", () => {
|
|||
it("forks threads with the session vault path and returns panel threads", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ thread: threadRecord("forked") });
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatThreadMutationTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
}).threadMutation;
|
||||
|
||||
const thread = await transport.forkThread("source");
|
||||
|
||||
|
|
@ -98,11 +84,10 @@ describe("chat app-server transports", () => {
|
|||
const firstClient = { request: vi.fn().mockReturnValue(fork.promise) } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createChatThreadMutationTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
});
|
||||
}).threadMutation;
|
||||
|
||||
const forking = transport.forkThread("source");
|
||||
currentClient = secondClient;
|
||||
|
|
@ -114,11 +99,10 @@ describe("chat app-server transports", () => {
|
|||
it("projects rollback turns into message stream items", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ thread: threadRecord("thread", [turn([userMessage("u1", "prompt")])]) });
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatThreadMutationTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
}).threadMutation;
|
||||
|
||||
const snapshot = await transport.rollbackThread("thread");
|
||||
|
||||
|
|
@ -134,9 +118,10 @@ describe("chat app-server transports", () => {
|
|||
nextCursor: "older",
|
||||
});
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatThreadHistoryTransport({
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
});
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
}).threadHistory;
|
||||
|
||||
const page = await transport.readHistoryPage("thread", "cursor", 20);
|
||||
|
||||
|
|
@ -160,9 +145,10 @@ describe("chat app-server transports", () => {
|
|||
const firstClient = { request: vi.fn().mockReturnValue(history.promise) } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createChatThreadHistoryTransport({
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
});
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
}).threadHistory;
|
||||
|
||||
const loading = transport.readHistoryPage("thread", "cursor", 20);
|
||||
currentClient = secondClient;
|
||||
|
|
@ -185,11 +171,10 @@ describe("chat app-server transports", () => {
|
|||
},
|
||||
});
|
||||
const client = { request } as unknown as AppServerClient;
|
||||
const transport = createChatThreadResumeTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
}).threadResume;
|
||||
|
||||
const snapshot = await transport.resumeThread("thread");
|
||||
|
||||
|
|
@ -214,11 +199,10 @@ describe("chat app-server transports", () => {
|
|||
const firstClient = { request: vi.fn().mockReturnValue(resume.promise) } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createChatThreadResumeTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
});
|
||||
}).threadResume;
|
||||
|
||||
const resuming = transport.resumeThread("thread");
|
||||
currentClient = secondClient;
|
||||
|
|
@ -228,28 +212,28 @@ describe("chat app-server transports", () => {
|
|||
});
|
||||
|
||||
it("returns no resume snapshot when no connected client is available", async () => {
|
||||
const transport = createChatThreadResumeTransport({
|
||||
vaultPath: "/vault",
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => null,
|
||||
connectedClient: vi.fn().mockResolvedValue(null),
|
||||
});
|
||||
}).threadResume;
|
||||
|
||||
await expect(transport.resumeThread("thread")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("distinguishes absent goals from unavailable goal clients", async () => {
|
||||
const client = { request: vi.fn().mockResolvedValue({ goal: null }) } as unknown as AppServerClient;
|
||||
const transport = createChatThreadGoalTransport({
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => client,
|
||||
connectedClient: vi.fn().mockResolvedValue(client),
|
||||
});
|
||||
const unavailable = createChatThreadGoalTransport({
|
||||
}).threadGoal;
|
||||
const unavailable = createTestGateway({
|
||||
currentClient: () => null,
|
||||
connectedClient: vi.fn().mockResolvedValue(null),
|
||||
});
|
||||
const readOnlyUnavailable = createChatThreadGoalReadTransport({
|
||||
}).threadGoal;
|
||||
const readOnlyUnavailable = createTestGateway({
|
||||
currentClient: () => null,
|
||||
});
|
||||
connectedClient: vi.fn().mockResolvedValue(null),
|
||||
}).threadGoalRead;
|
||||
|
||||
await expect(transport.readThreadGoal("thread")).resolves.toBeNull();
|
||||
await expect(unavailable.readThreadGoal("thread")).resolves.toBeUndefined();
|
||||
|
|
@ -262,9 +246,10 @@ describe("chat app-server transports", () => {
|
|||
const firstClient = { request } as unknown as AppServerClient;
|
||||
const secondClient = {} as unknown as AppServerClient;
|
||||
let currentClient = firstClient;
|
||||
const transport = createChatRuntimeSettingsTransport({
|
||||
const transport = createTestGateway({
|
||||
currentClient: () => currentClient,
|
||||
});
|
||||
connectedClient: vi.fn().mockResolvedValue(firstClient),
|
||||
}).runtimeSettings;
|
||||
|
||||
const updating = transport.updateThreadSettings("thread", { model: "gpt-5.5" });
|
||||
currentClient = secondClient;
|
||||
|
|
@ -312,6 +297,18 @@ describe("chat app-server transports", () => {
|
|||
|
||||
type AppServerThreadResumeResponse = ClientResponseByMethod["thread/resume"];
|
||||
|
||||
function createTestGateway(options: {
|
||||
vaultPath?: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
connectedClient?: () => Promise<AppServerClient | null>;
|
||||
}) {
|
||||
return createChatAppServerGateway({
|
||||
vaultPath: options.vaultPath ?? "/vault",
|
||||
currentClient: options.currentClient,
|
||||
connectedClient: options.connectedClient ?? (async () => options.currentClient()),
|
||||
});
|
||||
}
|
||||
|
||||
function threadRecord(id: string, turns: readonly TurnRecord[] = [], overrides: Partial<ThreadRecord> = {}): ThreadRecord {
|
||||
return {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ConnectionManager } from "../../../../src/app-server/connection/connection-manager";
|
||||
import { ConnectionWorkTracker } from "../../../../src/features/chat/application/connection/connection-work";
|
||||
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
|
||||
import { createReconnectAction } from "../../../../src/features/chat/host/bundles/reconnect-bundle";
|
||||
import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work";
|
||||
|
||||
describe("createReconnectAction", () => {
|
||||
it("wires host connection lifecycle cleanup into panel reconnect", async () => {
|
||||
const stateStore = createChatStateStore();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicyKnown: true,
|
||||
sandboxPolicyKnown: true,
|
||||
permissionProfileKnown: true,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: { id: "thread-1" } as never,
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
});
|
||||
const connectionWork = new ConnectionWorkTracker();
|
||||
const activeConnectionWork = connectionWork.begin();
|
||||
const deferredTasks = createChatViewDeferredTasks(() => ({ setTimeout, clearTimeout }) as Pick<Window, "setTimeout" | "clearTimeout">);
|
||||
const connection = new ConnectionManager(() => "codex", "/vault");
|
||||
const resetConnection = vi.spyOn(connection, "resetConnection");
|
||||
const clearDiagnostics = vi.spyOn(deferredTasks, "clearDiagnostics");
|
||||
const status = {
|
||||
set: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
};
|
||||
const ensureConnected = vi.fn().mockResolvedValue(undefined);
|
||||
const resumeThread = vi.fn().mockResolvedValue(undefined);
|
||||
const reconnect = createReconnectAction(
|
||||
{
|
||||
stateStore,
|
||||
connectionWork,
|
||||
deferredTasks,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
ensureConnected,
|
||||
invalidateThreadWork: vi.fn(),
|
||||
resumeThread,
|
||||
status,
|
||||
},
|
||||
);
|
||||
|
||||
await reconnect();
|
||||
|
||||
expect(connectionWork.isStale(activeConnectionWork)).toBe(true);
|
||||
expect(clearDiagnostics).toHaveBeenCalledOnce();
|
||||
expect(resetConnection).toHaveBeenCalledOnce();
|
||||
expect(status.set).toHaveBeenCalledWith("Reconnecting...", { kind: "connecting" });
|
||||
expect(ensureConnected).toHaveBeenCalledOnce();
|
||||
expect(resumeThread).toHaveBeenCalledWith("thread-1");
|
||||
});
|
||||
});
|
||||
|
|
@ -16,6 +16,7 @@ import { ChatComposerController } from "../../../../src/features/chat/panel/comp
|
|||
import { createChatMessageStreamScrollBinding } from "../../../../src/features/chat/panel/message-stream-scroll-binding";
|
||||
import { MessageStreamPresenter } from "../../../../src/features/chat/panel/surface/message-stream-presenter";
|
||||
import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model";
|
||||
import { waitForAsyncWork } from "../../../support/async";
|
||||
import { installObsidianDomShims } from "../../../support/dom";
|
||||
import { chatPanelSettingsAccess } from "../support/settings";
|
||||
|
||||
|
|
@ -199,6 +200,41 @@ describe("createChatPanelSessionGraph actions", () => {
|
|||
expect(focusComposer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("wires reconnect cleanup through the graph toolbar action", async () => {
|
||||
const { graph, stateStore, connectionWork, deferredTasks } = sessionGraphFixture();
|
||||
stateStore.dispatch({
|
||||
type: "active-thread/resumed",
|
||||
approvalPolicyKnown: true,
|
||||
sandboxPolicyKnown: true,
|
||||
permissionProfileKnown: true,
|
||||
approvalPolicy: null,
|
||||
sandboxPolicy: null,
|
||||
activePermissionProfile: null,
|
||||
thread: threadFixture({ id: "thread-1", preview: "Active" }),
|
||||
cwd: "/vault",
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
serviceTier: null,
|
||||
approvalsReviewer: null,
|
||||
});
|
||||
const activeConnectionWork = connectionWork.begin();
|
||||
const clearDiagnostics = vi.spyOn(deferredTasks, "clearDiagnostics");
|
||||
const resetConnection = vi.spyOn(graph.connection.manager, "resetConnection");
|
||||
const ensureConnected = vi.spyOn(graph.connection.actions, "ensureConnected").mockResolvedValue(undefined);
|
||||
const resumeThread = vi.spyOn(graph.thread.resume, "resumeThread").mockResolvedValue(undefined);
|
||||
|
||||
graph.shell.parts.toolbar.actions.status.connect();
|
||||
|
||||
await waitForAsyncWork(() => {
|
||||
expect(resumeThread).toHaveBeenCalledWith("thread-1");
|
||||
});
|
||||
expect(connectionWork.isStale(activeConnectionWork)).toBe(true);
|
||||
expect(clearDiagnostics).toHaveBeenCalledOnce();
|
||||
expect(resetConnection).toHaveBeenCalledOnce();
|
||||
expect(stateStore.getState().connection.statusText).toBe("Reconnecting...");
|
||||
expect(ensureConnected).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("disposes presenter and composer resources from the graph action", () => {
|
||||
const disposePresenter = vi.spyOn(MessageStreamPresenter.prototype, "dispose").mockImplementation(() => undefined);
|
||||
const disposeComposer = vi.spyOn(ChatComposerController.prototype, "dispose").mockImplementation(() => undefined);
|
||||
|
|
@ -214,21 +250,25 @@ describe("createChatPanelSessionGraph actions", () => {
|
|||
graph: ReturnType<typeof createChatPanelSessionGraph>;
|
||||
stateStore: ChatStateStore;
|
||||
resumeWork: ChatResumeWorkTracker;
|
||||
connectionWork: ConnectionWorkTracker;
|
||||
deferredTasks: ReturnType<typeof createChatViewDeferredTasks>;
|
||||
} {
|
||||
const stateStore = createChatStateStore();
|
||||
const resumeWork = new ChatResumeWorkTracker();
|
||||
const connectionWork = new ConnectionWorkTracker();
|
||||
const deferredTasks = createChatViewDeferredTasks(() => window);
|
||||
const environment = chatPanelEnvironmentFixture(options.environment);
|
||||
const graph = createChatPanelSessionGraph({
|
||||
environment,
|
||||
stateStore,
|
||||
deferredTasks: createChatViewDeferredTasks(() => window),
|
||||
deferredTasks,
|
||||
resumeWork,
|
||||
connectionWork: new ConnectionWorkTracker(),
|
||||
connectionWork,
|
||||
messageScrollBinding: createChatMessageStreamScrollBinding(),
|
||||
getClosing: () => false,
|
||||
viewWindow: () => window,
|
||||
});
|
||||
return { graph, stateStore, resumeWork };
|
||||
return { graph, stateStore, resumeWork, connectionWork, deferredTasks };
|
||||
}
|
||||
|
||||
interface PartialChatPanelEnvironment {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
|
||||
const projection = messageStreamSurfaceProjectionFromModel(
|
||||
messageStreamReadModelFromChatState(store.getState()),
|
||||
messageStreamSurfaceContext({
|
||||
testMessageStreamSurfaceContext({
|
||||
vaultPath: "/vault",
|
||||
dispatch: (action) => {
|
||||
store.dispatch(action);
|
||||
|
|
@ -75,7 +75,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
|
||||
it("wires message stream disclosure actions through the surface context", () => {
|
||||
const store = createChatStateStore(chatStateFixture());
|
||||
const surfaceContext = messageStreamSurfaceContext({
|
||||
const surfaceContext = testMessageStreamSurfaceContext({
|
||||
vaultPath: "/vault",
|
||||
dispatch: (action) => {
|
||||
store.dispatch(action);
|
||||
|
|
@ -95,7 +95,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
state = chatStateWith(state, { requests: { approvals: [pendingApproval()] } });
|
||||
const projection = messageStreamSurfaceProjectionFromModel(
|
||||
messageStreamReadModelFromChatState(state),
|
||||
messageStreamSurfaceContext({
|
||||
testMessageStreamSurfaceContext({
|
||||
vaultPath: "/vault",
|
||||
dispatch: () => undefined,
|
||||
}),
|
||||
|
|
@ -522,7 +522,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function messageStreamSurfaceContext(options: {
|
||||
function testMessageStreamSurfaceContext(options: {
|
||||
vaultPath: string;
|
||||
dispatch: (action: ChatAction) => void;
|
||||
}): ChatMessageStreamSurfaceContext {
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSettingsDynamicSectionLifecycle,
|
||||
settingsDynamicSectionFailed,
|
||||
settingsDynamicSectionLoaded,
|
||||
settingsDynamicSectionLoading,
|
||||
} from "../../src/settings/lifecycle";
|
||||
|
||||
describe("settings lifecycle", () => {
|
||||
it("tracks dynamic section lifecycle", () => {
|
||||
const idle = createSettingsDynamicSectionLifecycle();
|
||||
expect(idle).toEqual({ kind: "idle", status: "" });
|
||||
|
||||
const loading = settingsDynamicSectionLoading("Loading hooks...");
|
||||
expect(loading).toEqual({ kind: "loading", status: "Loading hooks..." });
|
||||
|
||||
const loaded = settingsDynamicSectionLoaded("Loaded 1 hook.");
|
||||
expect(loaded).toEqual({ kind: "loaded", status: "Loaded 1 hook." });
|
||||
|
||||
const failed = settingsDynamicSectionFailed("Could not load hooks.");
|
||||
expect(failed).toEqual({ kind: "failed", status: "Could not load hooks." });
|
||||
|
||||
expect(createSettingsDynamicSectionLifecycle()).toEqual(idle);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue