mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Simplify app-server action and settings boundaries
This commit is contained in:
parent
4a7e5f905f
commit
f2178b2db4
22 changed files with 588 additions and 580 deletions
|
|
@ -1,24 +0,0 @@
|
|||
import type { AppServerClient } from "../connection/client";
|
||||
import { normalizeExplicitThreadName } from "../../domain/threads/model";
|
||||
|
||||
export interface RenameThreadResult {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ThreadRename {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function threadRenameFromValue(value: string): ThreadRename | null {
|
||||
const name = normalizeExplicitThreadName(value);
|
||||
return name ? { name } : null;
|
||||
}
|
||||
|
||||
export async function renameThreadOnAppServer(
|
||||
client: AppServerClient,
|
||||
threadId: string,
|
||||
rename: ThreadRename,
|
||||
): Promise<RenameThreadResult> {
|
||||
await client.setThreadName(threadId, rename.name);
|
||||
return { name: rename.name };
|
||||
}
|
||||
|
|
@ -34,7 +34,6 @@ export interface ChatServerDiagnosticsActionsHost extends ChatServerActionHost {
|
|||
|
||||
export interface ChatServerDiagnosticsActions {
|
||||
refreshDiagnosticProbes: (options?: RefreshDiagnosticProbesOptions) => Promise<void>;
|
||||
refreshPublishedDiagnosticProbes: (options?: RefreshDiagnosticProbesOptions) => Promise<void>;
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
recordMcpStartupStatus: (name: string, startupStatus: McpServerStartupStatus, message: string | null) => void;
|
||||
}
|
||||
|
|
@ -44,7 +43,6 @@ export function createChatServerDiagnosticsActions(host: ChatServerDiagnosticsAc
|
|||
refreshDiagnosticProbes: async (options) => {
|
||||
await refreshDiagnosticProbes(host, options);
|
||||
},
|
||||
refreshPublishedDiagnosticProbes: (options) => refreshPublishedDiagnosticProbes(host, options),
|
||||
mcpStatusLines: () => mcpStatusLines(host),
|
||||
recordMcpStartupStatus: (name, startupStatus, message) => {
|
||||
recordMcpStartupStatus(host, name, startupStatus, message);
|
||||
|
|
@ -137,13 +135,6 @@ async function readRateLimitDiagnosticProbe(client: AppServerClient): Promise<Di
|
|||
return { method: "account/rateLimits/read", probe: result.probe };
|
||||
}
|
||||
|
||||
async function refreshPublishedDiagnosticProbes(
|
||||
host: ChatServerDiagnosticsActionsHost,
|
||||
options: RefreshDiagnosticProbesOptions = {},
|
||||
): Promise<void> {
|
||||
await refreshDiagnosticProbes(host, options);
|
||||
}
|
||||
|
||||
async function mcpStatusLines(host: ChatServerDiagnosticsActionsHost): Promise<string[]> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return ["MCP servers", "Codex app-server is not connected."];
|
||||
|
|
|
|||
|
|
@ -20,13 +20,10 @@ export interface ChatServerMetadataActions {
|
|||
applyAppServerMetadata: (metadata: SharedServerMetadata) => void;
|
||||
loadAppServerMetadata: () => Promise<SharedServerMetadata | null>;
|
||||
refreshAppServerMetadata: () => Promise<SharedServerMetadata | null>;
|
||||
refreshPublishedAppServerMetadata: () => Promise<SharedServerMetadata | null>;
|
||||
applyAppServerMetadataSnapshot: () => void;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
refreshPublishedSkills: (forceReload?: boolean) => Promise<void>;
|
||||
loadSkills: (forceReload?: boolean) => Promise<SkillMetadataProbeResult>;
|
||||
refreshRateLimits: () => Promise<void>;
|
||||
refreshPublishedRateLimits: () => Promise<void>;
|
||||
refreshRateLimits: (options?: { preserveExistingOnFailure?: boolean }) => Promise<void>;
|
||||
loadRateLimit: () => Promise<RateLimitMetadataProbeResult>;
|
||||
}
|
||||
|
||||
|
|
@ -37,17 +34,14 @@ export function createChatServerMetadataActions(host: ChatServerMetadataActionsH
|
|||
},
|
||||
loadAppServerMetadata: () => loadAppServerMetadata(host),
|
||||
refreshAppServerMetadata: () => refreshAppServerMetadata(host),
|
||||
refreshPublishedAppServerMetadata: () => refreshPublishedAppServerMetadata(host),
|
||||
applyAppServerMetadataSnapshot: () => {
|
||||
applyAppServerMetadataSnapshot(host);
|
||||
},
|
||||
refreshSkills: async (forceReload) => {
|
||||
await refreshSkills(host, forceReload);
|
||||
},
|
||||
refreshPublishedSkills: (forceReload) => refreshPublishedSkills(host, forceReload),
|
||||
loadSkills: (forceReload) => loadSkills(host, forceReload),
|
||||
refreshRateLimits: () => refreshRateLimits(host),
|
||||
refreshPublishedRateLimits: () => refreshPublishedRateLimits(host),
|
||||
refreshRateLimits: (options) => refreshRateLimits(host, options),
|
||||
loadRateLimit: () => loadRateLimit(host),
|
||||
};
|
||||
}
|
||||
|
|
@ -80,10 +74,6 @@ async function refreshAppServerMetadata(host: ChatServerMetadataActionsHost): Pr
|
|||
return metadata;
|
||||
}
|
||||
|
||||
async function refreshPublishedAppServerMetadata(host: ChatServerMetadataActionsHost): Promise<SharedServerMetadata | null> {
|
||||
return refreshAppServerMetadata(host);
|
||||
}
|
||||
|
||||
function applyAppServerMetadataSnapshot(host: ChatServerMetadataActionsHost): void {
|
||||
const metadata = host.appServerMetadataSnapshot();
|
||||
if (metadata) applyAppServerMetadata(host, metadata);
|
||||
|
|
@ -114,50 +104,33 @@ async function refreshSkills(host: ChatServerMetadataActionsHost, forceReload =
|
|||
return null;
|
||||
}
|
||||
|
||||
async function refreshPublishedSkills(host: ChatServerMetadataActionsHost, forceReload = false): Promise<void> {
|
||||
await refreshSkills(host, forceReload);
|
||||
}
|
||||
|
||||
async function loadSkills(host: ChatServerMetadataActionsHost, forceReload = false): Promise<SkillMetadataProbeResult> {
|
||||
return readSkillMetadataProbe(host.currentClient(), host.vaultPath, forceReload);
|
||||
}
|
||||
|
||||
async function refreshRateLimits(host: ChatServerMetadataActionsHost): Promise<void> {
|
||||
async function refreshRateLimits(
|
||||
host: ChatServerMetadataActionsHost,
|
||||
options: { preserveExistingOnFailure?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const client = host.currentClient();
|
||||
const rateLimit = await readRateLimitMetadataProbe(client);
|
||||
if (client && host.currentClient() !== client) return;
|
||||
const next = updateRateLimitMetadata(host, rateLimit, { preserveRateLimitOnFailure: false });
|
||||
const preserveExistingOnFailure = options.preserveExistingOnFailure === true;
|
||||
const next = updateRateLimitMetadata(host, rateLimit, { preserveRateLimitOnFailure: preserveExistingOnFailure });
|
||||
if (next) {
|
||||
applyAppServerMetadata(host, next);
|
||||
return;
|
||||
}
|
||||
const diagnostics = diagnosticsWithProbe(currentMetadataDiagnostics(host), rateLimit.probe);
|
||||
host.stateStore.dispatch({
|
||||
type: "connection/metadata-applied",
|
||||
rateLimit: rateLimit.data,
|
||||
serverDiagnostics: diagnostics,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshPublishedRateLimits(host: ChatServerMetadataActionsHost): Promise<void> {
|
||||
const client = host.currentClient();
|
||||
const rateLimit = await readRateLimitMetadataProbe(client);
|
||||
if (client && host.currentClient() !== client) return;
|
||||
const next = updateRateLimitMetadata(host, rateLimit, { preserveRateLimitOnFailure: true });
|
||||
if (next) {
|
||||
applyAppServerMetadata(host, next);
|
||||
return;
|
||||
}
|
||||
const diagnostics = diagnosticsWithProbe(currentMetadataDiagnostics(host), rateLimit.probe);
|
||||
if (rateLimit.probe.status === "ok") {
|
||||
host.stateStore.dispatch({
|
||||
type: "connection/metadata-applied",
|
||||
rateLimit: rateLimit.data,
|
||||
serverDiagnostics: diagnostics,
|
||||
});
|
||||
return;
|
||||
}
|
||||
host.stateStore.dispatch({ type: "connection/metadata-applied", serverDiagnostics: diagnostics });
|
||||
host.stateStore.dispatch(
|
||||
preserveExistingOnFailure && rateLimit.probe.status !== "ok"
|
||||
? { type: "connection/metadata-applied", serverDiagnostics: diagnostics }
|
||||
: {
|
||||
type: "connection/metadata-applied",
|
||||
rateLimit: rateLimit.data,
|
||||
serverDiagnostics: diagnostics,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function loadRateLimit(host: ChatServerMetadataActionsHost): Promise<RateLimitMetadataProbeResult> {
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ export interface ChatConnectionAdapter {
|
|||
}
|
||||
|
||||
export interface ChatConnectionMetadataActions {
|
||||
refreshPublishedAppServerMetadata: () => Promise<unknown>;
|
||||
refreshPublishedSkills: (forceReload?: boolean) => Promise<void>;
|
||||
refreshAppServerMetadata: () => Promise<unknown>;
|
||||
refreshSkills: (forceReload?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ChatConnectionDiagnosticsActions {
|
||||
refreshPublishedDiagnosticProbes: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise<void>;
|
||||
refreshDiagnosticProbes: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface ChatConnectionControllerHost {
|
||||
|
|
@ -125,8 +125,8 @@ async function refreshDiagnostics(
|
|||
await controller.ensureConnected();
|
||||
if (!host.connection.currentClient()) return;
|
||||
host.clearDeferredDiagnostics();
|
||||
await host.metadata.refreshPublishedAppServerMetadata();
|
||||
await host.diagnostics.refreshPublishedDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
await host.metadata.refreshAppServerMetadata();
|
||||
await host.diagnostics.refreshDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
}
|
||||
|
||||
async function refreshStatusPanel(
|
||||
|
|
@ -143,7 +143,7 @@ async function refreshStatusPanel(
|
|||
|
||||
async function refreshSkills(host: ChatConnectionControllerHost, forceReload = false): Promise<void> {
|
||||
if (!host.connection.currentClient()) return;
|
||||
await host.metadata.refreshPublishedSkills(forceReload);
|
||||
await host.metadata.refreshSkills(forceReload);
|
||||
}
|
||||
|
||||
async function initializeConnection(host: ChatConnectionControllerHost, connection: ActiveConnectionWork): Promise<void> {
|
||||
|
|
@ -154,7 +154,7 @@ async function initializeConnection(host: ChatConnectionControllerHost, connecti
|
|||
host.stateStore.dispatch({ type: "connection/initialized", initializeResponse: initialization });
|
||||
const client = host.connection.currentClient();
|
||||
if (!client) throw new Error("Codex app-server connection did not initialize.");
|
||||
await host.metadata.refreshPublishedAppServerMetadata();
|
||||
await host.metadata.refreshAppServerMetadata();
|
||||
if (host.connectionWork.isStale(connection)) return;
|
||||
await host.loadSharedThreadList();
|
||||
if (host.connectionWork.isStale(connection)) return;
|
||||
|
|
|
|||
|
|
@ -90,23 +90,10 @@ export function createConversationTurnActions(
|
|||
startNewThread: thread.startNewThread,
|
||||
startThreadForGoal: (objective) => startThreadForGoal(refs.threadStarter, objective),
|
||||
resumeThread: thread.selectThread,
|
||||
forkThread: (threadId) => refs.threadActions.forkThread(threadId),
|
||||
rollbackThread: (threadId) => refs.threadActions.rollbackThread(threadId),
|
||||
compactThread: (threadId) => refs.threadActions.compactThread(threadId),
|
||||
archiveThread: (threadId, saveMarkdown) => refs.threadActions.archiveThread(threadId, saveMarkdown),
|
||||
renameThread: (threadId, name) => refs.threadActions.renameThread(threadId, name).then(() => undefined),
|
||||
threadActions: refs.threadActions,
|
||||
reconnect: refs.reconnectPanel,
|
||||
toggleFastMode: () => refs.runtimeSettings.toggleFastMode(),
|
||||
toggleCollaborationMode: () => refs.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
|
||||
requestModel: (model) => refs.runtimeSettings.requestModel(model),
|
||||
resetModelToConfig: () => refs.runtimeSettings.resetModelToConfig(),
|
||||
requestReasoningEffort: (effort) => refs.runtimeSettings.requestReasoningEffort(effort),
|
||||
resetReasoningEffortToConfig: () => refs.runtimeSettings.resetReasoningEffortToConfig(),
|
||||
activeGoal: () => refs.goals.activeGoal(),
|
||||
setGoalObjective: (threadId, objective, tokenBudget) => refs.goals.setObjective(threadId, objective, tokenBudget),
|
||||
setGoalStatus: (threadId, goalStatus) => refs.goals.setStatus(threadId, goalStatus),
|
||||
clearGoal: (threadId) => refs.goals.clear(threadId),
|
||||
runtimeSettings: refs.runtimeSettings,
|
||||
goals: refs.goals,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
addStructuredSystemMessage: status.addStructuredSystemMessage,
|
||||
setStatus: status.set,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import type { CodexInput } from "../../../../domain/chat/input";
|
||||
import type { ThreadGoal, ThreadGoalStatus } from "../../../../domain/threads/goal";
|
||||
import type { ThreadGoal } from "../../../../domain/threads/goal";
|
||||
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import { normalizeReasoningEffort } from "../../../../domain/catalog/metadata";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../../domain/threads/model";
|
||||
import type { ReferencedThreadMetadata } from "../../../../domain/threads/reference";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
|
||||
import type { GoalActions } from "../threads/goal-actions";
|
||||
import type { ThreadManagementActions } from "../threads/thread-management-actions";
|
||||
import {
|
||||
slashCommandDefinition,
|
||||
slashCommandHelpSections,
|
||||
|
|
@ -34,26 +37,22 @@ export interface SlashCommandExecutionContext {
|
|||
startThreadForGoal: (objective: string) => Promise<string | null>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
referThread: (thread: Thread, message: string) => Promise<ThreadReferenceInput | null>;
|
||||
forkThread: (threadId: string) => Promise<void>;
|
||||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
compactThread: (threadId: string) => Promise<void>;
|
||||
archiveThread: (threadId: string, saveMarkdown?: boolean) => Promise<void>;
|
||||
renameThread: (threadId: string, name: string) => Promise<void>;
|
||||
threadActions: Pick<ThreadManagementActions, "forkThread" | "rollbackThread" | "compactThread" | "archiveThread" | "renameThread">;
|
||||
reconnect: () => Promise<void>;
|
||||
toggleFastMode: () => void | Promise<void>;
|
||||
toggleCollaborationMode: () => void | Promise<void>;
|
||||
toggleAutoReview: () => void | Promise<void>;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
requestModel: (model: string) => boolean | undefined | Promise<boolean | undefined>;
|
||||
resetModelToConfig: () => boolean | undefined | Promise<boolean | undefined>;
|
||||
requestReasoningEffort: (effort: ReasoningEffort) => boolean | undefined | Promise<boolean | undefined>;
|
||||
resetReasoningEffortToConfig: () => boolean | undefined | Promise<boolean | undefined>;
|
||||
runtimeSettings: Pick<
|
||||
ChatRuntimeSettingsActions,
|
||||
| "toggleFastMode"
|
||||
| "toggleCollaborationMode"
|
||||
| "toggleAutoReview"
|
||||
| "requestModel"
|
||||
| "resetModelToConfig"
|
||||
| "requestReasoningEffort"
|
||||
| "resetReasoningEffortToConfig"
|
||||
>;
|
||||
supportedReasoningEfforts: () => readonly ReasoningEffort[];
|
||||
activeGoal: () => ThreadGoal | null;
|
||||
setGoalObjective: (threadId: string, objective: string, tokenBudget: number | null) => Promise<boolean>;
|
||||
setGoalStatus: (threadId: string, status: ThreadGoalStatus) => Promise<boolean>;
|
||||
clearGoal: (threadId: string) => Promise<boolean>;
|
||||
goals: Pick<GoalActions, "activeGoal" | "setObjective" | "setStatus" | "clear">;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
|
|
@ -84,189 +83,167 @@ export async function executeSlashCommand(
|
|||
return;
|
||||
}
|
||||
|
||||
if (command === "clear") {
|
||||
await context.startNewThread();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "resume") {
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
switch (command) {
|
||||
case "clear":
|
||||
await context.startNewThread();
|
||||
return;
|
||||
case "resume": {
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.resumeThread(thread.thread.id);
|
||||
return;
|
||||
}
|
||||
await context.resumeThread(thread.thread.id);
|
||||
return;
|
||||
}
|
||||
case "reconnect":
|
||||
await context.reconnect();
|
||||
return;
|
||||
case "refer": {
|
||||
const parsed = parseReferArgs(args);
|
||||
if (!parsed) {
|
||||
context.addSystemMessage(usageError(command, "requires a thread and a message"));
|
||||
return;
|
||||
}
|
||||
const thread = resolveThreadArgument(parsed.threadQuery, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
if (thread.thread.id === context.activeThreadId) {
|
||||
context.addSystemMessage(currentThreadReferenceMessage());
|
||||
return;
|
||||
}
|
||||
const reference = await context.referThread(thread.thread, parsed.message);
|
||||
if (!reference) return;
|
||||
return { sendText: parsed.message, sendInput: reference.input, referencedThread: reference.referencedThread };
|
||||
}
|
||||
case "fork":
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToForkMessage());
|
||||
return;
|
||||
}
|
||||
await context.threadActions.forkThread(context.activeThreadId);
|
||||
return;
|
||||
case "rollback":
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToRollbackMessage());
|
||||
return;
|
||||
}
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(interruptBeforeRollbackMessage());
|
||||
return;
|
||||
}
|
||||
await context.threadActions.rollbackThread(context.activeThreadId);
|
||||
return;
|
||||
case "compact":
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToCompactMessage());
|
||||
return;
|
||||
}
|
||||
await context.threadActions.compactThread(context.activeThreadId);
|
||||
return;
|
||||
case "archive": {
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(finishBeforeArchivingThreadsMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "reconnect") {
|
||||
await context.reconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "refer") {
|
||||
const parsed = parseReferArgs(args);
|
||||
if (!parsed) {
|
||||
context.addSystemMessage(usageError(command, "requires a thread and a message"));
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.threadActions.archiveThread(thread.thread.id);
|
||||
return;
|
||||
}
|
||||
const thread = resolveThreadArgument(parsed.threadQuery, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
case "rename": {
|
||||
const parsed = parseThreadAndNameArgs(args);
|
||||
if (!parsed) {
|
||||
context.addSystemMessage(usageError(command, "requires a thread and a name"));
|
||||
return;
|
||||
}
|
||||
const thread = resolveThreadArgument(parsed.threadQuery, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.threadActions.renameThread(thread.thread.id, parsed.text);
|
||||
return;
|
||||
}
|
||||
if (thread.thread.id === context.activeThreadId) {
|
||||
context.addSystemMessage(currentThreadReferenceMessage());
|
||||
case "fast":
|
||||
await context.runtimeSettings.toggleFastMode();
|
||||
return;
|
||||
case "auto-review":
|
||||
await context.runtimeSettings.toggleAutoReview();
|
||||
return;
|
||||
case "plan":
|
||||
await context.runtimeSettings.toggleCollaborationMode();
|
||||
if (args) return { sendText: args };
|
||||
return;
|
||||
case "goal":
|
||||
return await executeGoalCommand(args, context);
|
||||
case "status":
|
||||
context.addStructuredSystemMessage("Thread status", detailsFromLines(context.statusSummaryLines()));
|
||||
return;
|
||||
case "doctor":
|
||||
context.addStructuredSystemMessage("Connection diagnostics", context.connectionDiagnosticDetails());
|
||||
return;
|
||||
case "mcp":
|
||||
context.addStructuredSystemMessage("MCP servers", detailsFromLines(await context.mcpStatusLines()));
|
||||
return;
|
||||
case "model": {
|
||||
const requested = parseModelOverride(args);
|
||||
if (requested !== undefined) {
|
||||
const applied = await applyModelOverride(context, requested);
|
||||
if (applied === false) return;
|
||||
context.addSystemMessage(modelOverrideMessage(requested));
|
||||
return;
|
||||
}
|
||||
context.addStructuredSystemMessage("Model settings", detailsFromLines(context.modelStatusLines()));
|
||||
return;
|
||||
}
|
||||
const reference = await context.referThread(thread.thread, parsed.message);
|
||||
if (!reference) return;
|
||||
return { sendText: parsed.message, sendInput: reference.input, referencedThread: reference.referencedThread };
|
||||
}
|
||||
|
||||
if (command === "fork") {
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToForkMessage());
|
||||
return;
|
||||
}
|
||||
await context.forkThread(context.activeThreadId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "rollback") {
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToRollbackMessage());
|
||||
return;
|
||||
}
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(interruptBeforeRollbackMessage());
|
||||
return;
|
||||
}
|
||||
await context.rollbackThread(context.activeThreadId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "compact") {
|
||||
if (!context.activeThreadId) {
|
||||
context.addSystemMessage(noActiveThreadToCompactMessage());
|
||||
return;
|
||||
}
|
||||
await context.compactThread(context.activeThreadId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "archive") {
|
||||
if (context.busy) {
|
||||
context.addSystemMessage(finishBeforeArchivingThreadsMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = resolveThreadArgument(args, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.archiveThread(thread.thread.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "rename") {
|
||||
const parsed = parseThreadAndNameArgs(args);
|
||||
if (!parsed) {
|
||||
context.addSystemMessage(usageError(command, "requires a thread and a name"));
|
||||
return;
|
||||
}
|
||||
const thread = resolveThreadArgument(parsed.threadQuery, context.listedThreads);
|
||||
if (!thread.ok) {
|
||||
context.addSystemMessage(thread.message);
|
||||
return;
|
||||
}
|
||||
await context.renameThread(thread.thread.id, parsed.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "fast") {
|
||||
await context.toggleFastMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "auto-review") {
|
||||
await context.toggleAutoReview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "plan") {
|
||||
await context.toggleCollaborationMode();
|
||||
if (args) return { sendText: args };
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "goal") {
|
||||
return await executeGoalCommand(args, context);
|
||||
}
|
||||
|
||||
if (command === "status") {
|
||||
context.addStructuredSystemMessage("Thread status", detailsFromLines(context.statusSummaryLines()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "doctor") {
|
||||
context.addStructuredSystemMessage("Connection diagnostics", context.connectionDiagnosticDetails());
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "mcp") {
|
||||
context.addStructuredSystemMessage("MCP servers", detailsFromLines(await context.mcpStatusLines()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "model") {
|
||||
const requested = parseModelOverride(args);
|
||||
if (requested !== undefined) {
|
||||
const applied = await applyModelOverride(context, requested);
|
||||
if (applied === false) return;
|
||||
context.addSystemMessage(modelOverrideMessage(requested));
|
||||
return;
|
||||
}
|
||||
context.addStructuredSystemMessage("Model settings", detailsFromLines(context.modelStatusLines()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "reasoning") {
|
||||
const requested = parseReasoningEffortOverride(args);
|
||||
if (requested !== undefined) {
|
||||
if (requested !== null && !context.supportedReasoningEfforts().includes(requested)) {
|
||||
case "reasoning": {
|
||||
const requested = parseReasoningEffortOverride(args);
|
||||
if (requested !== undefined) {
|
||||
if (requested !== null && !context.supportedReasoningEfforts().includes(requested)) {
|
||||
context.addSystemMessage(`Unsupported reasoning level: ${args}. Usage: ${slashCommandDefinition(command).usage}`);
|
||||
return;
|
||||
}
|
||||
const applied = await applyReasoningEffortOverride(context, requested);
|
||||
if (applied === false) return;
|
||||
context.addSystemMessage(reasoningEffortOverrideMessage(requested));
|
||||
return;
|
||||
}
|
||||
if (args) {
|
||||
context.addSystemMessage(`Unsupported reasoning level: ${args}. Usage: ${slashCommandDefinition(command).usage}`);
|
||||
return;
|
||||
}
|
||||
const applied = await applyReasoningEffortOverride(context, requested);
|
||||
if (applied === false) return;
|
||||
context.addSystemMessage(reasoningEffortOverrideMessage(requested));
|
||||
context.addStructuredSystemMessage("Reasoning effort", detailsFromLines(context.effortStatusLines()));
|
||||
return;
|
||||
}
|
||||
if (args) {
|
||||
context.addSystemMessage(`Unsupported reasoning level: ${args}. Usage: ${slashCommandDefinition(command).usage}`);
|
||||
case "help":
|
||||
context.addStructuredSystemMessage("Available slash commands", slashCommandHelpSections());
|
||||
return;
|
||||
}
|
||||
context.addStructuredSystemMessage("Reasoning effort", detailsFromLines(context.effortStatusLines()));
|
||||
return;
|
||||
}
|
||||
|
||||
context.addStructuredSystemMessage("Available slash commands", slashCommandHelpSections());
|
||||
const _exhaustive: never = command;
|
||||
return _exhaustive;
|
||||
}
|
||||
|
||||
function applyModelOverride(
|
||||
context: SlashCommandExecutionContext,
|
||||
requested: string | null,
|
||||
): boolean | undefined | Promise<boolean | undefined> {
|
||||
return requested === null ? context.resetModelToConfig() : context.requestModel(requested);
|
||||
return requested === null ? context.runtimeSettings.resetModelToConfig() : context.runtimeSettings.requestModel(requested);
|
||||
}
|
||||
|
||||
function applyReasoningEffortOverride(
|
||||
context: SlashCommandExecutionContext,
|
||||
requested: ReasoningEffort | null,
|
||||
): boolean | undefined | Promise<boolean | undefined> {
|
||||
return requested === null ? context.resetReasoningEffortToConfig() : context.requestReasoningEffort(requested);
|
||||
return requested === null
|
||||
? context.runtimeSettings.resetReasoningEffortToConfig()
|
||||
: context.runtimeSettings.requestReasoningEffort(requested);
|
||||
}
|
||||
|
||||
function parseModelOverride(args: string): string | null | undefined {
|
||||
|
|
@ -299,7 +276,7 @@ async function executeGoalCommand(args: string, context: SlashCommandExecutionCo
|
|||
return;
|
||||
}
|
||||
if (parsed.kind === "show") {
|
||||
const goal = context.activeGoal();
|
||||
const goal = context.goals.activeGoal();
|
||||
if (!goal) {
|
||||
context.addSystemMessage("No goal set.");
|
||||
return;
|
||||
|
|
@ -307,14 +284,14 @@ async function executeGoalCommand(args: string, context: SlashCommandExecutionCo
|
|||
context.addStructuredSystemMessage("Thread goal", goalDetails(goal));
|
||||
return;
|
||||
}
|
||||
const goal = context.activeGoal();
|
||||
const goal = context.goals.activeGoal();
|
||||
if (parsed.kind === "set") {
|
||||
const threadId = context.activeThreadId ?? (await context.startThreadForGoal(parsed.objective));
|
||||
if (!threadId) {
|
||||
context.addSystemMessage("No active thread for goal management.");
|
||||
return;
|
||||
}
|
||||
await context.setGoalObjective(threadId, parsed.objective, goal?.tokenBudget ?? null);
|
||||
await context.goals.setObjective(threadId, parsed.objective, goal?.tokenBudget ?? null);
|
||||
return;
|
||||
}
|
||||
const threadId = context.activeThreadId;
|
||||
|
|
@ -330,14 +307,14 @@ async function executeGoalCommand(args: string, context: SlashCommandExecutionCo
|
|||
return { composerDraft: `/goal set ${goal.objective}` };
|
||||
}
|
||||
if (parsed.kind === "pause") {
|
||||
await context.setGoalStatus(threadId, "paused");
|
||||
await context.goals.setStatus(threadId, "paused");
|
||||
return;
|
||||
}
|
||||
if (parsed.kind === "resume") {
|
||||
await context.setGoalStatus(threadId, "active");
|
||||
await context.goals.setStatus(threadId, "active");
|
||||
return;
|
||||
}
|
||||
await context.clearGoal(threadId);
|
||||
await context.goals.clear(threadId);
|
||||
}
|
||||
|
||||
type GoalArgs =
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { codexTextInputWithAttachments, type CodexInput } from "../../../../doma
|
|||
import { readReferencedThreadConversationSummaries } from "../../../../app-server/threads/data";
|
||||
import { referencedThreadPromptBundle, REFERENCED_THREAD_TURN_LIMIT } from "../../../../domain/threads/reference";
|
||||
import type { Thread } from "../../../../domain/threads/model";
|
||||
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
|
||||
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
|
||||
import { referencedThreadStatus, referencedThreadUnreadableMessage } from "./messages";
|
||||
import {
|
||||
executeSlashCommand as runSlashCommand,
|
||||
type SlashCommandExecutionContext,
|
||||
type SlashCommandExecutionResult,
|
||||
type ThreadReferenceInput,
|
||||
} from "./slash-command-execution";
|
||||
|
|
@ -17,14 +18,37 @@ import { submissionStateSnapshot } from "../state/selectors";
|
|||
import type { ChatStateStore } from "../state/store";
|
||||
import { currentModel, runtimeConfigOrDefault } from "../../domain/runtime/effective";
|
||||
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
|
||||
import type { GoalActions } from "../threads/goal-actions";
|
||||
import type { ThreadManagementActions } from "../threads/thread-management-actions";
|
||||
|
||||
type DynamicSlashCommandExecutionContext = "activeThreadId" | "busy" | "listedThreads" | "referThread" | "supportedReasoningEfforts";
|
||||
|
||||
export interface SlashCommandHandlerHost extends Omit<SlashCommandExecutionContext, DynamicSlashCommandExecutionContext> {
|
||||
export interface SlashCommandHandlerHost {
|
||||
stateStore: ChatStateStore;
|
||||
currentClient: () => AppServerClient | null;
|
||||
codexInput: (text: string) => CodexInput;
|
||||
setStatus: (status: string) => void;
|
||||
startNewThread: () => Promise<void>;
|
||||
startThreadForGoal: (objective: string) => Promise<string | null>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
reconnect: () => Promise<void>;
|
||||
threadActions: Pick<ThreadManagementActions, "forkThread" | "rollbackThread" | "compactThread" | "archiveThread" | "renameThread">;
|
||||
runtimeSettings: Pick<
|
||||
ChatRuntimeSettingsActions,
|
||||
| "toggleFastMode"
|
||||
| "toggleCollaborationMode"
|
||||
| "toggleAutoReview"
|
||||
| "requestModel"
|
||||
| "resetModelToConfig"
|
||||
| "requestReasoningEffort"
|
||||
| "resetReasoningEffortToConfig"
|
||||
>;
|
||||
goals: Pick<GoalActions, "activeGoal" | "setObjective" | "setStatus" | "clear">;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addStructuredSystemMessage: (text: string, details: MessageStreamNoticeSection[]) => void;
|
||||
statusSummaryLines: () => string[];
|
||||
connectionDiagnosticDetails: () => MessageStreamNoticeSection[];
|
||||
mcpStatusLines: () => Promise<string[]>;
|
||||
modelStatusLines: () => string[];
|
||||
effortStatusLines: () => string[];
|
||||
}
|
||||
|
||||
export async function executeSlashCommandWithState(
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ import { ConnectionManager } from "../../../app-server/connection/connection-man
|
|||
import type { AppServerObservedQueryResult } from "../../../app-server/query/cache";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
|
||||
import { appServerQueryContextRawEquals, type AppServerQueryContext } from "../../../app-server/query/keys";
|
||||
import { renameThreadOnAppServer, threadRenameFromValue } from "../../../app-server/services/thread-rename";
|
||||
import type { ModelMetadata } from "../../../domain/catalog/metadata";
|
||||
|
||||
import type { Thread } from "../../../domain/threads/model";
|
||||
import { getThreadTitle } from "../../../domain/threads/model";
|
||||
import { getThreadTitle, normalizeExplicitThreadName } from "../../../domain/threads/model";
|
||||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
|
||||
import { shortThreadId } from "../../../utils";
|
||||
|
|
@ -640,15 +639,15 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
stateStore: this.stateStore,
|
||||
titleService,
|
||||
renameGeneratedTitle: async (threadId, title, options) => {
|
||||
const rename = threadRenameFromValue(title);
|
||||
if (!rename) return false;
|
||||
const name = normalizeExplicitThreadName(title);
|
||||
if (!name) return false;
|
||||
const client = currentClient();
|
||||
if (!client) return false;
|
||||
|
||||
const result = await renameThreadOnAppServer(client, threadId, rename);
|
||||
await client.setThreadName(threadId, name);
|
||||
if (currentClient() !== client) return false;
|
||||
if (options.shouldPublish()) {
|
||||
this.environment.plugin.threadCatalog.renameThreadInCatalog(threadId, result.name);
|
||||
this.environment.plugin.threadCatalog.renameThreadInCatalog(threadId, name);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -1168,9 +1167,9 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
void loadSharedThreadList();
|
||||
},
|
||||
refreshRateLimits: () => {
|
||||
void serverMetadata.refreshPublishedRateLimits();
|
||||
void serverMetadata.refreshRateLimits({ preserveExistingOnFailure: true });
|
||||
},
|
||||
refreshSkills: (forceReload) => void serverMetadata.refreshPublishedSkills(forceReload),
|
||||
refreshSkills: (forceReload) => void serverMetadata.refreshSkills(forceReload),
|
||||
applyAppServerMetadataSnapshot: () => {
|
||||
serverMetadata.applyAppServerMetadataSnapshot();
|
||||
},
|
||||
|
|
@ -1227,17 +1226,17 @@ export class ChatPanelSession implements ChatSurfaceHandle {
|
|||
isConnected: () => connection.isConnected(),
|
||||
},
|
||||
metadata: {
|
||||
refreshPublishedAppServerMetadata: () => serverMetadata.refreshPublishedAppServerMetadata(),
|
||||
refreshPublishedSkills: (forceReload) => serverMetadata.refreshPublishedSkills(forceReload),
|
||||
refreshAppServerMetadata: () => serverMetadata.refreshAppServerMetadata(),
|
||||
refreshSkills: (forceReload) => serverMetadata.refreshSkills(forceReload),
|
||||
},
|
||||
diagnostics: {
|
||||
refreshPublishedDiagnosticProbes: (options) => serverDiagnostics.refreshPublishedDiagnosticProbes(options),
|
||||
refreshDiagnosticProbes: (options) => serverDiagnostics.refreshDiagnosticProbes(options),
|
||||
},
|
||||
loadSharedThreadList,
|
||||
scheduleDeferredDiagnostics: () => {
|
||||
this.deferredTasks.scheduleDiagnostics(() => {
|
||||
if (connection.isConnected()) {
|
||||
void serverDiagnostics.refreshPublishedDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
void serverDiagnostics.refreshDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { AppServerClient } from "../../app-server/connection/client";
|
||||
import { archiveThreadOnAppServer, type ArchiveThreadResult } from "../../app-server/services/thread-archive";
|
||||
import type { ArchiveExportAdapter } from "../../app-server/services/thread-archive-markdown";
|
||||
import { renameThreadOnAppServer, threadRenameFromValue, type ThreadRename } from "../../app-server/services/thread-rename";
|
||||
import { normalizeExplicitThreadName } from "../../domain/threads/model";
|
||||
import type { CodexPanelSettings } from "../../settings/model";
|
||||
|
||||
export interface ThreadOperationsHost {
|
||||
|
|
@ -48,25 +48,16 @@ async function renameThread(
|
|||
value: string,
|
||||
options: RenameThreadOptions = {},
|
||||
): Promise<boolean> {
|
||||
const rename = threadRenameFromValue(value);
|
||||
if (!rename) return false;
|
||||
const name = normalizeExplicitThreadName(value);
|
||||
if (!name) return false;
|
||||
|
||||
await host.connection.ensureConnected();
|
||||
return renameConnectedThread(host, threadId, rename, options);
|
||||
}
|
||||
|
||||
async function renameConnectedThread(
|
||||
host: ThreadOperationsHost,
|
||||
threadId: string,
|
||||
rename: ThreadRename,
|
||||
options: RenameThreadOptions = {},
|
||||
): Promise<boolean> {
|
||||
const client = host.connection.currentClient();
|
||||
if (!client) return false;
|
||||
|
||||
const result = await renameThreadOnAppServer(client, threadId, rename);
|
||||
await client.setThreadName(threadId, name);
|
||||
if (options.shouldPublish?.() ?? true) {
|
||||
host.catalog.renameThreadInCatalog(threadId, result.name);
|
||||
host.catalog.renameThreadInCatalog(threadId, name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ export class CodexPanelRuntime {
|
|||
this.threadCatalog = createSharedThreadCatalog({
|
||||
queries: this.appServerSharedQueries,
|
||||
surfaces: {
|
||||
invalidateThreadsFromOpenSurface: () => {
|
||||
this.invalidateThreadsFromOpenSurface();
|
||||
},
|
||||
applyThreadArchived: (threadId, archiveOptions) => {
|
||||
this.applyThreadArchived(threadId, archiveOptions);
|
||||
},
|
||||
|
|
@ -178,17 +175,6 @@ export class CodexPanelRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
private invalidateThreadsFromOpenSurface(): void {
|
||||
const chatSurface = this.connectedPanelSurface();
|
||||
if (chatSurface) {
|
||||
void chatSurface.refreshSharedThreadList();
|
||||
return;
|
||||
}
|
||||
|
||||
const threadsView = this.threadsViews().at(0);
|
||||
if (threadsView) void threadsView.refresh();
|
||||
}
|
||||
|
||||
private applyThreadArchived(threadId: string, archiveOptions: { closeOpenPanels?: boolean } = {}): void {
|
||||
const leavesToClose = archiveOptions.closeOpenPanels ? this.panels.panelLeavesForThread(threadId) : [];
|
||||
for (const view of this.panels.panelViews()) {
|
||||
|
|
@ -207,14 +193,6 @@ export class CodexPanelRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
private connectedPanelSurface(): (ChatWorkspacePanelSurface & ChatSharedThreadSurface) | null {
|
||||
for (const view of this.panels.panelViews()) {
|
||||
const surface: ChatWorkspacePanelSurface & ChatSharedThreadSurface = view.surface;
|
||||
if (surface.openPanelSnapshot().connected) return surface;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private refreshThreadsViewLiveState(): void {
|
||||
for (const view of this.threadsViews()) {
|
||||
view.refreshLiveState();
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type SettingsAppServerData = Pick<
|
|||
"modelsSnapshot" | "observeModelsResult" | "fetchModels" | "refreshModels" | "notifyContextChanged"
|
||||
>;
|
||||
|
||||
type SettingsThreadCatalog = Pick<SharedThreadCatalog, "invalidateThreadsFromOpenSurface">;
|
||||
type SettingsThreadCatalog = Pick<SharedThreadCatalog, "refreshActiveThreads">;
|
||||
|
||||
function archivedThreadTitleForStatus(thread: Thread | undefined, threadId: string): string {
|
||||
return thread ? archivedThreadDisplayTitle(thread) : threadId;
|
||||
|
|
@ -60,7 +60,10 @@ export interface SettingsDynamicDataSnapshot {
|
|||
|
||||
export class SettingsDynamicDataController {
|
||||
private settingsDataAutoLoadStarted = false;
|
||||
private settingsDynamicOperationId = 0;
|
||||
private settingsRefreshOperationId = 0;
|
||||
private modelsOperationId = 0;
|
||||
private hooksOperationId = 0;
|
||||
private archivedThreadsOperationId = 0;
|
||||
private settingsDataRefreshLifecycle: SettingsDataRefreshLifecycleState = { kind: "idle" };
|
||||
|
||||
private archivedThreads: Thread[] = [];
|
||||
|
|
@ -99,7 +102,10 @@ export class SettingsDynamicDataController {
|
|||
|
||||
resetSettingsDataContext(): void {
|
||||
this.settingsDataAutoLoadStarted = false;
|
||||
this.settingsDynamicOperationId += 1;
|
||||
this.settingsRefreshOperationId += 1;
|
||||
this.modelsOperationId += 1;
|
||||
this.hooksOperationId += 1;
|
||||
this.archivedThreadsOperationId += 1;
|
||||
this.settingsDataRefreshLifecycle = { kind: "idle" };
|
||||
this.models = [...(this.host.appServerData.modelsSnapshot() ?? [])];
|
||||
this.modelsLifecycle = createSettingsDynamicSectionLifecycle();
|
||||
|
|
@ -124,7 +130,10 @@ export class SettingsDynamicDataController {
|
|||
|
||||
async refreshSettingsData(options: { forceModels?: boolean } = {}): Promise<void> {
|
||||
this.settingsDataAutoLoadStarted = true;
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextSettingsRefreshOperationId();
|
||||
const modelsOperationId = this.nextModelsOperationId();
|
||||
const hooksOperationId = this.nextHooksOperationId();
|
||||
const archivedThreadsOperationId = this.nextArchivedThreadsOperationId();
|
||||
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, {
|
||||
type: "started",
|
||||
operationId,
|
||||
|
|
@ -132,17 +141,17 @@ export class SettingsDynamicDataController {
|
|||
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading models...",
|
||||
operationId,
|
||||
operationId: modelsOperationId,
|
||||
});
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading archived threads...",
|
||||
operationId,
|
||||
operationId: archivedThreadsOperationId,
|
||||
});
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading hooks...",
|
||||
operationId,
|
||||
operationId: hooksOperationId,
|
||||
});
|
||||
this.callbacks.display();
|
||||
|
||||
|
|
@ -152,14 +161,16 @@ export class SettingsDynamicDataController {
|
|||
options.forceModels === false ? this.host.appServerData.fetchModels() : this.host.appServerData.refreshModels(),
|
||||
this.withSettingsConnection((client) => loadSettingsCompanionData(client, this.host.vaultPath)),
|
||||
] as const);
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleSettingsRefreshOperation(operationId)) return;
|
||||
|
||||
if (modelsResult.status === "fulfilled") {
|
||||
if (this.isStaleModelsOperation(modelsOperationId)) {
|
||||
// A newer models operation owns this section.
|
||||
} else if (modelsResult.status === "fulfilled") {
|
||||
this.models = [...modelsResult.value];
|
||||
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Loaded ${String(modelsResult.value.length)} model${modelsResult.value.length === 1 ? "" : "s"}.`,
|
||||
operationId,
|
||||
operationId: modelsOperationId,
|
||||
});
|
||||
} else if (isStaleAppServerSharedQueryContextError(modelsResult.reason)) {
|
||||
return;
|
||||
|
|
@ -168,7 +179,7 @@ export class SettingsDynamicDataController {
|
|||
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load models: ${errorMessage(modelsResult.reason)}`,
|
||||
operationId,
|
||||
operationId: modelsOperationId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -180,60 +191,70 @@ export class SettingsDynamicDataController {
|
|||
archivedThreads: { ok: false as const, status: `Could not load archived threads: ${errorMessage(companionResult.reason)}` },
|
||||
};
|
||||
|
||||
if (companion.hooks.ok) {
|
||||
if (this.isStaleHooksOperation(hooksOperationId)) {
|
||||
// A newer hooks operation owns this section.
|
||||
} else if (companion.hooks.ok) {
|
||||
this.hooks = companion.hooks.data.hooks;
|
||||
this.hookWarnings = companion.hooks.data.warnings;
|
||||
this.hookErrors = companion.hooks.data.errors;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
status: companion.hooks.status,
|
||||
operationId,
|
||||
operationId: hooksOperationId,
|
||||
});
|
||||
} else {
|
||||
failedCount += 1;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: companion.hooks.status,
|
||||
operationId,
|
||||
operationId: hooksOperationId,
|
||||
});
|
||||
}
|
||||
|
||||
if (companion.archivedThreads.ok) {
|
||||
if (this.isStaleArchivedThreadsOperation(archivedThreadsOperationId)) {
|
||||
// A newer archived threads operation owns this section.
|
||||
} else if (companion.archivedThreads.ok) {
|
||||
this.archivedThreads = companion.archivedThreads.data;
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: companion.archivedThreads.status,
|
||||
operationId,
|
||||
operationId: archivedThreadsOperationId,
|
||||
});
|
||||
} else {
|
||||
failedCount += 1;
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "failed",
|
||||
status: companion.archivedThreads.status,
|
||||
operationId,
|
||||
operationId: archivedThreadsOperationId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleSettingsRefreshOperation(operationId)) return;
|
||||
failedCount = 3;
|
||||
const message = errorMessage(error);
|
||||
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load models: ${message}`,
|
||||
operationId,
|
||||
});
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load hooks: ${message}`,
|
||||
operationId,
|
||||
});
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load archived threads: ${message}`,
|
||||
operationId,
|
||||
});
|
||||
if (!this.isStaleModelsOperation(modelsOperationId)) {
|
||||
this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load models: ${message}`,
|
||||
operationId: modelsOperationId,
|
||||
});
|
||||
}
|
||||
if (!this.isStaleHooksOperation(hooksOperationId)) {
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load hooks: ${message}`,
|
||||
operationId: hooksOperationId,
|
||||
});
|
||||
}
|
||||
if (!this.isStaleArchivedThreadsOperation(archivedThreadsOperationId)) {
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load archived threads: ${message}`,
|
||||
operationId: archivedThreadsOperationId,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
const staleOperation = this.isStaleSettingsDynamicOperation(operationId);
|
||||
const staleOperation = this.isStaleSettingsRefreshOperation(operationId);
|
||||
this.settingsDataRefreshLifecycle = transitionSettingsDataRefreshLifecycle(this.settingsDataRefreshLifecycle, {
|
||||
type: "completed",
|
||||
failedCount,
|
||||
|
|
@ -266,7 +287,7 @@ export class SettingsDynamicDataController {
|
|||
}
|
||||
|
||||
async trustHook(hook: HookItem): Promise<void> {
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextHooksOperationId();
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading hooks...",
|
||||
|
|
@ -275,7 +296,7 @@ export class SettingsDynamicDataController {
|
|||
this.callbacks.display();
|
||||
try {
|
||||
await this.withSettingsConnection((client) => trustHookItem(client, hook));
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
status: "Trusted hook definition.",
|
||||
|
|
@ -283,7 +304,7 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
await this.loadHooks();
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not trust hook: ${errorMessage(error)}`,
|
||||
|
|
@ -295,7 +316,7 @@ export class SettingsDynamicDataController {
|
|||
}
|
||||
|
||||
async setHookEnabled(hook: HookItem, enabled: boolean): Promise<void> {
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextHooksOperationId();
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading hooks...",
|
||||
|
|
@ -304,7 +325,7 @@ export class SettingsDynamicDataController {
|
|||
this.callbacks.display();
|
||||
try {
|
||||
await this.withSettingsConnection((client) => setHookItemEnabled(client, hook, enabled));
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "loaded",
|
||||
status: enabled ? "Enabled hook." : "Disabled hook.",
|
||||
|
|
@ -312,7 +333,7 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
await this.loadHooks();
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not update hook: ${errorMessage(error)}`,
|
||||
|
|
@ -324,7 +345,7 @@ export class SettingsDynamicDataController {
|
|||
}
|
||||
|
||||
async restoreArchivedThread(threadId: string): Promise<void> {
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextArchivedThreadsOperationId();
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading archived threads...",
|
||||
|
|
@ -333,16 +354,23 @@ export class SettingsDynamicDataController {
|
|||
this.callbacks.display();
|
||||
try {
|
||||
const restoredThread = await this.withSettingsConnection((client) => restoreArchivedThreadOnAppServer(client, threadId));
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleArchivedThreadsOperation(operationId)) return;
|
||||
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Restored "${archivedThreadDisplayTitle(restoredThread)}".`,
|
||||
operationId,
|
||||
});
|
||||
this.host.threadCatalog.invalidateThreadsFromOpenSurface();
|
||||
this.callbacks.display();
|
||||
try {
|
||||
await this.host.threadCatalog.refreshActiveThreads();
|
||||
} catch (error) {
|
||||
if (!this.isStaleArchivedThreadsOperation(operationId) && !isStaleAppServerSharedQueryContextError(error)) {
|
||||
this.callbacks.notify("Could not refresh active Codex threads.");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleArchivedThreadsOperation(operationId)) return;
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not restore archived thread: ${errorMessage(error)}`,
|
||||
|
|
@ -350,12 +378,12 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
this.callbacks.notify("Could not restore archived Codex thread.");
|
||||
} finally {
|
||||
if (!this.isStaleSettingsDynamicOperation(operationId)) this.callbacks.display();
|
||||
if (!this.isStaleArchivedThreadsOperation(operationId)) this.callbacks.display();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteArchivedThread(threadId: string): Promise<void> {
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextArchivedThreadsOperationId();
|
||||
const title = archivedThreadTitleForStatus(
|
||||
this.archivedThreads.find((thread) => thread.id === threadId),
|
||||
threadId,
|
||||
|
|
@ -368,16 +396,15 @@ export class SettingsDynamicDataController {
|
|||
this.callbacks.display();
|
||||
try {
|
||||
await this.withSettingsConnection((client) => deleteArchivedThreadOnAppServer(client, threadId));
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleArchivedThreadsOperation(operationId)) return;
|
||||
this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId);
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "loaded",
|
||||
status: `Deleted "${title}".`,
|
||||
operationId,
|
||||
});
|
||||
this.host.threadCatalog.invalidateThreadsFromOpenSurface();
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleArchivedThreadsOperation(operationId)) return;
|
||||
this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not delete archived thread: ${errorMessage(error)}`,
|
||||
|
|
@ -385,7 +412,7 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
this.callbacks.notify("Could not delete archived Codex thread.");
|
||||
} finally {
|
||||
if (!this.isStaleSettingsDynamicOperation(operationId)) this.callbacks.display();
|
||||
if (!this.isStaleArchivedThreadsOperation(operationId)) this.callbacks.display();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -407,7 +434,7 @@ export class SettingsDynamicDataController {
|
|||
}
|
||||
|
||||
private async loadHooks(): Promise<void> {
|
||||
const operationId = this.nextSettingsDynamicOperationId();
|
||||
const operationId = this.nextHooksOperationId();
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "started",
|
||||
status: "Loading hooks...",
|
||||
|
|
@ -416,7 +443,7 @@ export class SettingsDynamicDataController {
|
|||
this.callbacks.display();
|
||||
try {
|
||||
const hooks = await this.withSettingsConnection((client) => loadHookData(client, this.host.vaultPath));
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooks = hooks.hooks;
|
||||
this.hookWarnings = hooks.warnings;
|
||||
this.hookErrors = hooks.errors;
|
||||
|
|
@ -426,7 +453,7 @@ export class SettingsDynamicDataController {
|
|||
operationId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.isStaleSettingsDynamicOperation(operationId)) return;
|
||||
if (this.isStaleHooksOperation(operationId)) return;
|
||||
this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, {
|
||||
type: "failed",
|
||||
status: `Could not load hooks: ${errorMessage(error)}`,
|
||||
|
|
@ -434,7 +461,7 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
this.callbacks.notify("Could not load Codex hooks.");
|
||||
} finally {
|
||||
if (!this.isStaleSettingsDynamicOperation(operationId)) this.callbacks.display();
|
||||
if (!this.isStaleHooksOperation(operationId)) this.callbacks.display();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -444,13 +471,40 @@ export class SettingsDynamicDataController {
|
|||
});
|
||||
}
|
||||
|
||||
private nextSettingsDynamicOperationId(): number {
|
||||
this.settingsDynamicOperationId += 1;
|
||||
return this.settingsDynamicOperationId;
|
||||
private nextSettingsRefreshOperationId(): number {
|
||||
this.settingsRefreshOperationId += 1;
|
||||
return this.settingsRefreshOperationId;
|
||||
}
|
||||
|
||||
private isStaleSettingsDynamicOperation(operationId: number): boolean {
|
||||
return operationId !== this.settingsDynamicOperationId;
|
||||
private nextModelsOperationId(): number {
|
||||
this.modelsOperationId += 1;
|
||||
return this.modelsOperationId;
|
||||
}
|
||||
|
||||
private nextHooksOperationId(): number {
|
||||
this.hooksOperationId += 1;
|
||||
return this.hooksOperationId;
|
||||
}
|
||||
|
||||
private nextArchivedThreadsOperationId(): number {
|
||||
this.archivedThreadsOperationId += 1;
|
||||
return this.archivedThreadsOperationId;
|
||||
}
|
||||
|
||||
private isStaleSettingsRefreshOperation(operationId: number): boolean {
|
||||
return operationId !== this.settingsRefreshOperationId;
|
||||
}
|
||||
|
||||
private isStaleModelsOperation(operationId: number): boolean {
|
||||
return operationId !== this.modelsOperationId;
|
||||
}
|
||||
|
||||
private isStaleHooksOperation(operationId: number): boolean {
|
||||
return operationId !== this.hooksOperationId;
|
||||
}
|
||||
|
||||
private isStaleArchivedThreadsOperation(operationId: number): boolean {
|
||||
return operationId !== this.archivedThreadsOperationId;
|
||||
}
|
||||
|
||||
private selectedModel(modelIdOrName: string | null): ModelMetadata | null {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { AppServerObservedQueryResult } from "../app-server/query/cache";
|
|||
import type { AppServerSharedQueries } from "../app-server/query/shared-queries";
|
||||
|
||||
interface ThreadSurfaceActions {
|
||||
invalidateThreadsFromOpenSurface(): void;
|
||||
applyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void;
|
||||
applyThreadRenamed(threadId: string, name: string | null): void;
|
||||
refreshThreadsViewLiveState(): void;
|
||||
|
|
@ -23,7 +22,6 @@ export interface SharedThreadCatalog {
|
|||
listener: (result: AppServerObservedQueryResult<readonly Thread[]>) => void,
|
||||
options?: { emitCurrent?: boolean },
|
||||
): () => void;
|
||||
invalidateThreadsFromOpenSurface(): void;
|
||||
renameThreadInCatalog(threadId: string, name: string | null): void;
|
||||
archiveThreadInCatalog(threadId: string, options?: { closeOpenPanels?: boolean }): void;
|
||||
refreshThreadsViewLiveState(): void;
|
||||
|
|
@ -38,9 +36,6 @@ export function createSharedThreadCatalog(options: SharedThreadCatalogOptions):
|
|||
options.queries.setActiveThreads(threads);
|
||||
},
|
||||
observeActiveThreadsResult: (listener, observeOptions) => options.queries.observeActiveThreadsResult(listener, observeOptions),
|
||||
invalidateThreadsFromOpenSurface: () => {
|
||||
options.surfaces.invalidateThreadsFromOpenSurface();
|
||||
},
|
||||
renameThreadInCatalog: (threadId, name) => {
|
||||
options.queries.updateActiveThreads((current) => {
|
||||
return current ? current.map((thread) => (thread.id === threadId ? { ...thread, name } : thread)) : null;
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../src/app-server/connection/client";
|
||||
import { renameThreadOnAppServer, threadRenameFromValue } from "../../src/app-server/services/thread-rename";
|
||||
|
||||
describe("thread rename operation", () => {
|
||||
it("normalizes thread names before saving them", async () => {
|
||||
const setThreadName = vi.fn().mockResolvedValue({});
|
||||
const client = fakeClient({ setThreadName });
|
||||
|
||||
const rename = threadRenameFromValue(" Renamed thread ");
|
||||
|
||||
expect(rename).toEqual({ name: "Renamed thread" });
|
||||
if (!rename) throw new Error("Expected normalized rename");
|
||||
await expect(renameThreadOnAppServer(client, "thread", rename)).resolves.toEqual({ name: "Renamed thread" });
|
||||
|
||||
expect(setThreadName).toHaveBeenCalledWith("thread", "Renamed thread");
|
||||
});
|
||||
|
||||
it("exposes normalized rename construction for surface guards", () => {
|
||||
expect(threadRenameFromValue(" Saved title ")).toEqual({ name: "Saved title" });
|
||||
expect(threadRenameFromValue(" ")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function fakeClient(options: { setThreadName: ReturnType<typeof vi.fn> }): AppServerClient {
|
||||
return {
|
||||
setThreadName: options.setThreadName,
|
||||
} as unknown as AppServerClient;
|
||||
}
|
||||
|
|
@ -25,15 +25,15 @@ function createController({ connected = false, client = {} as AppServerClient }
|
|||
currentClient: () => currentClient,
|
||||
isConnected: () => Boolean(currentClient),
|
||||
};
|
||||
const refreshPublishedAppServerMetadata = vi.fn().mockResolvedValue(null);
|
||||
const refreshPublishedDiagnosticProbes = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshPublishedSkills = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshAppServerMetadata = vi.fn().mockResolvedValue(null);
|
||||
const refreshDiagnosticProbes = vi.fn().mockResolvedValue(undefined);
|
||||
const refreshSkills = vi.fn().mockResolvedValue(undefined);
|
||||
const metadata = {
|
||||
refreshPublishedAppServerMetadata,
|
||||
refreshPublishedSkills,
|
||||
refreshAppServerMetadata,
|
||||
refreshSkills,
|
||||
} satisfies ChatConnectionMetadataActions;
|
||||
const diagnostics = {
|
||||
refreshPublishedDiagnosticProbes,
|
||||
refreshDiagnosticProbes,
|
||||
} satisfies ChatConnectionDiagnosticsActions;
|
||||
const host: ChatConnectionControllerHost = {
|
||||
stateStore,
|
||||
|
|
@ -57,15 +57,15 @@ function createController({ connected = false, client = {} as AppServerClient }
|
|||
connect,
|
||||
controller: createChatConnectionController(host),
|
||||
host,
|
||||
refreshPublishedAppServerMetadata,
|
||||
refreshPublishedDiagnosticProbes,
|
||||
refreshAppServerMetadata,
|
||||
refreshDiagnosticProbes,
|
||||
stateStore,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ChatConnectionController", () => {
|
||||
it("connects once and publishes startup metadata", async () => {
|
||||
const { connect, controller, host, refreshPublishedAppServerMetadata, stateStore } = createController();
|
||||
const { connect, controller, host, refreshAppServerMetadata, stateStore } = createController();
|
||||
|
||||
await controller.ensureConnected();
|
||||
|
||||
|
|
@ -76,29 +76,29 @@ describe("ChatConnectionController", () => {
|
|||
platformOs: "macos",
|
||||
userAgent: "test",
|
||||
});
|
||||
expect(refreshPublishedAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(refreshAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(host.loadSharedThreadList).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredDiagnostics).toHaveBeenCalledOnce();
|
||||
expect(host.setStatus).toHaveBeenCalledWith("Connected.", { kind: "connected" });
|
||||
});
|
||||
|
||||
it("refreshes metadata before metadata-backed diagnostics", async () => {
|
||||
const { controller, host, refreshPublishedAppServerMetadata, refreshPublishedDiagnosticProbes } = createController({ connected: true });
|
||||
const { controller, host, refreshAppServerMetadata, refreshDiagnosticProbes } = createController({ connected: true });
|
||||
|
||||
await controller.refreshDiagnostics();
|
||||
|
||||
expect(host.clearDeferredDiagnostics).toHaveBeenCalledTimes(2);
|
||||
expect(refreshPublishedAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(refreshPublishedDiagnosticProbes).toHaveBeenCalledWith({ appServerMetadataSnapshot: true });
|
||||
expect(refreshAppServerMetadata).toHaveBeenCalledOnce();
|
||||
expect(refreshDiagnosticProbes).toHaveBeenCalledWith({ appServerMetadataSnapshot: true });
|
||||
});
|
||||
|
||||
it("refreshes active threads without refreshing metadata", async () => {
|
||||
const { controller, host, refreshPublishedAppServerMetadata } = createController({ connected: true });
|
||||
const { controller, host, refreshAppServerMetadata } = createController({ connected: true });
|
||||
|
||||
await controller.fetchActiveThreads();
|
||||
|
||||
expect(host.loadSharedThreadList).toHaveBeenCalledOnce();
|
||||
expect(refreshPublishedAppServerMetadata).not.toHaveBeenCalled();
|
||||
expect(refreshAppServerMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears disconnected connection state on server exit while keeping last startup metadata", () => {
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ describe("chat server actions", () => {
|
|||
updateAppServerMetadata,
|
||||
});
|
||||
|
||||
const refreshing = diagnostics.refreshPublishedDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
const refreshing = diagnostics.refreshDiagnosticProbes({ appServerMetadataSnapshot: true });
|
||||
currentClient = secondClient;
|
||||
hooksRefresh.resolve({ data: [{ cwd: "/vault", hooks: [{}] }] });
|
||||
|
||||
|
|
@ -473,7 +473,7 @@ describe("chat server actions", () => {
|
|||
refreshAppServerMetadata,
|
||||
});
|
||||
|
||||
const refreshing = controller.refreshPublishedAppServerMetadata();
|
||||
const refreshing = controller.refreshAppServerMetadata();
|
||||
|
||||
await expect(refreshing).resolves.toBeNull();
|
||||
expect(stateStore.getState().connection.availableModels).toEqual([]);
|
||||
|
|
@ -543,7 +543,7 @@ describe("chat server actions", () => {
|
|||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
const refreshing = controller.refreshPublishedSkills(true);
|
||||
const refreshing = controller.refreshSkills(true);
|
||||
currentClient = secondClient;
|
||||
skillRefresh.resolve({ data: [{ skills: [skillFixture("stale-skill")] }] });
|
||||
|
||||
|
|
@ -571,7 +571,7 @@ describe("chat server actions", () => {
|
|||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
await controller.refreshPublishedRateLimits();
|
||||
await controller.refreshRateLimits({ preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toMatchObject({ primary: { usedPercent: 64 } });
|
||||
expect(cachedMetadata.current?.rateLimit).toStrictEqual(rateLimit);
|
||||
|
|
@ -597,7 +597,7 @@ describe("chat server actions", () => {
|
|||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
await controller.refreshPublishedRateLimits();
|
||||
await controller.refreshRateLimits({ preserveExistingOnFailure: true });
|
||||
|
||||
expect(stateStore.getState().connection.rateLimit).toBe(previousRateLimit);
|
||||
expect(stateStore.getState().connection.serverDiagnostics.probes["account/rateLimits/read"]).toMatchObject({ status: "failed" });
|
||||
|
|
@ -622,7 +622,7 @@ describe("chat server actions", () => {
|
|||
refreshAppServerMetadata: async () => null,
|
||||
});
|
||||
|
||||
const refreshing = controller.refreshPublishedRateLimits();
|
||||
const refreshing = controller.refreshRateLimits({ preserveExistingOnFailure: true });
|
||||
currentClient = secondClient;
|
||||
rateLimitRefresh.resolve({
|
||||
rateLimits: rateLimitFixture({ primary: { usedPercent: 88, windowDurationMins: 300, resetsAt: null } }),
|
||||
|
|
|
|||
|
|
@ -20,26 +20,32 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
|
|||
input: [{ type: "text", text: "referenced" }],
|
||||
referencedThread: { threadId: "thread-2", title: "Referenced", includedTurns: 1, turnLimit: 20 },
|
||||
}),
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread: vi.fn().mockResolvedValue(undefined),
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
renameThread: vi.fn().mockResolvedValue(undefined),
|
||||
threadActions: {
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread: vi.fn().mockResolvedValue(undefined),
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
renameThread: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
reconnect: vi.fn().mockResolvedValue(undefined),
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
addStructuredSystemMessage: vi.fn(),
|
||||
requestModel: vi.fn(),
|
||||
resetModelToConfig: vi.fn(),
|
||||
requestReasoningEffort: vi.fn(),
|
||||
resetReasoningEffortToConfig: vi.fn(),
|
||||
runtimeSettings: {
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
requestModel: vi.fn(),
|
||||
resetModelToConfig: vi.fn(),
|
||||
requestReasoningEffort: vi.fn(),
|
||||
resetReasoningEffortToConfig: vi.fn(),
|
||||
},
|
||||
supportedReasoningEfforts: () => ["low", "medium", "high"],
|
||||
activeGoal: vi.fn(() => null),
|
||||
setGoalObjective: vi.fn().mockResolvedValue(true),
|
||||
setGoalStatus: vi.fn().mockResolvedValue(true),
|
||||
clearGoal: vi.fn().mockResolvedValue(true),
|
||||
goals: {
|
||||
activeGoal: vi.fn(() => null),
|
||||
setObjective: vi.fn().mockResolvedValue(true),
|
||||
setStatus: vi.fn().mockResolvedValue(true),
|
||||
clear: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
statusSummaryLines: () => ["status"],
|
||||
connectionDiagnosticDetails: () => [{ title: "Process", rows: [{ key: "connection", value: "connected" }] }],
|
||||
mcpStatusLines: vi.fn().mockResolvedValue(["mcp"]),
|
||||
|
|
@ -173,7 +179,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("fork", "", ctx);
|
||||
|
||||
expect(ctx.forkThread).toHaveBeenCalledWith("active-thread");
|
||||
expect(ctx.threadActions.forkThread).toHaveBeenCalledWith("active-thread");
|
||||
});
|
||||
|
||||
it("rejects /fork arguments", async () => {
|
||||
|
|
@ -181,7 +187,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("fork", "anything", ctx);
|
||||
|
||||
expect(ctx.forkThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.forkThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/fork does not take arguments. Usage: /fork");
|
||||
});
|
||||
|
||||
|
|
@ -190,7 +196,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rollback", "", ctx);
|
||||
|
||||
expect(ctx.rollbackThread).toHaveBeenCalledWith("active-thread");
|
||||
expect(ctx.threadActions.rollbackThread).toHaveBeenCalledWith("active-thread");
|
||||
});
|
||||
|
||||
it("rejects /rollback without an active thread", async () => {
|
||||
|
|
@ -198,7 +204,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rollback", "", ctx);
|
||||
|
||||
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("No active thread to roll back.");
|
||||
});
|
||||
|
||||
|
|
@ -207,7 +213,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rollback", "", ctx);
|
||||
|
||||
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Interrupt the current turn before rolling back.");
|
||||
});
|
||||
|
||||
|
|
@ -216,7 +222,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rollback", "2", ctx);
|
||||
|
||||
expect(ctx.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.rollbackThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rollback does not take arguments. Usage: /rollback");
|
||||
});
|
||||
|
||||
|
|
@ -225,13 +231,14 @@ describe("slash commands", () => {
|
|||
|
||||
const result = await executeSlashCommand("plan", "", ctx);
|
||||
|
||||
expect(ctx.toggleCollaborationMode).toHaveBeenCalledOnce();
|
||||
expect(ctx.runtimeSettings.toggleCollaborationMode).toHaveBeenCalledOnce();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shows the current goal for /goal", async () => {
|
||||
const currentGoal = goal();
|
||||
const ctx = context({ activeGoal: vi.fn(() => currentGoal) });
|
||||
const ctx = context();
|
||||
ctx.goals.activeGoal = vi.fn(() => currentGoal);
|
||||
|
||||
await executeSlashCommand("goal", "", ctx);
|
||||
|
||||
|
|
@ -253,21 +260,23 @@ describe("slash commands", () => {
|
|||
|
||||
it("sets, pauses, resumes, and clears goals", async () => {
|
||||
const currentGoal = goal();
|
||||
const ctx = context({ activeGoal: vi.fn(() => currentGoal) });
|
||||
const ctx = context();
|
||||
ctx.goals.activeGoal = vi.fn(() => currentGoal);
|
||||
|
||||
await executeSlashCommand("goal", "set Ship this", ctx);
|
||||
await executeSlashCommand("goal", "pause", ctx);
|
||||
await executeSlashCommand("goal", "resume", ctx);
|
||||
await executeSlashCommand("goal", "clear", ctx);
|
||||
|
||||
expect(ctx.setGoalObjective).toHaveBeenCalledWith("thread-1", "Ship this", null);
|
||||
expect(ctx.setGoalStatus).toHaveBeenCalledWith("thread-1", "paused");
|
||||
expect(ctx.setGoalStatus).toHaveBeenCalledWith("thread-1", "active");
|
||||
expect(ctx.clearGoal).toHaveBeenCalledWith("thread-1");
|
||||
expect(ctx.goals.setObjective).toHaveBeenCalledWith("thread-1", "Ship this", null);
|
||||
expect(ctx.goals.setStatus).toHaveBeenCalledWith("thread-1", "paused");
|
||||
expect(ctx.goals.setStatus).toHaveBeenCalledWith("thread-1", "active");
|
||||
expect(ctx.goals.clear).toHaveBeenCalledWith("thread-1");
|
||||
});
|
||||
|
||||
it("loads the current goal into the composer for /goal edit", async () => {
|
||||
const ctx = context({ activeGoal: vi.fn(() => goal({ objective: "Ship goal support" })) });
|
||||
const ctx = context();
|
||||
ctx.goals.activeGoal = vi.fn(() => goal({ objective: "Ship goal support" }));
|
||||
|
||||
const result = await executeSlashCommand("goal", "edit", ctx);
|
||||
|
||||
|
|
@ -287,21 +296,22 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("goal", "set", ctx);
|
||||
|
||||
expect(ctx.setGoalObjective).not.toHaveBeenCalled();
|
||||
expect(ctx.goals.setObjective).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/goal set <objective> requires an objective. Usage: /goal set <objective>");
|
||||
});
|
||||
|
||||
it("rejects extra arguments for goal subcommands without free text", async () => {
|
||||
const currentGoal = goal();
|
||||
const ctx = context({ activeGoal: vi.fn(() => currentGoal) });
|
||||
const ctx = context();
|
||||
ctx.goals.activeGoal = vi.fn(() => currentGoal);
|
||||
|
||||
await executeSlashCommand("goal", "edit later", ctx);
|
||||
await executeSlashCommand("goal", "pause later", ctx);
|
||||
await executeSlashCommand("goal", "resume now", ctx);
|
||||
await executeSlashCommand("goal", "clear please", ctx);
|
||||
|
||||
expect(ctx.setGoalStatus).not.toHaveBeenCalled();
|
||||
expect(ctx.clearGoal).not.toHaveBeenCalled();
|
||||
expect(ctx.goals.setStatus).not.toHaveBeenCalled();
|
||||
expect(ctx.goals.clear).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/goal edit does not take arguments. Usage: /goal edit");
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/goal pause does not take arguments. Usage: /goal pause");
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/goal resume does not take arguments. Usage: /goal resume");
|
||||
|
|
@ -324,7 +334,7 @@ describe("slash commands", () => {
|
|||
await executeSlashCommand("goal", "set Ship this", ctx);
|
||||
|
||||
expect(ctx.startThreadForGoal).toHaveBeenCalledWith("Ship this");
|
||||
expect(ctx.setGoalObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
expect(ctx.goals.setObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
expect(ctx.addSystemMessage).not.toHaveBeenCalledWith("No active thread for goal management.");
|
||||
});
|
||||
|
||||
|
|
@ -334,7 +344,7 @@ describe("slash commands", () => {
|
|||
await executeSlashCommand("goal", "pause", ctx);
|
||||
|
||||
expect(ctx.startThreadForGoal).not.toHaveBeenCalled();
|
||||
expect(ctx.setGoalStatus).not.toHaveBeenCalled();
|
||||
expect(ctx.goals.setStatus).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("No active thread for goal management.");
|
||||
});
|
||||
|
||||
|
|
@ -343,7 +353,7 @@ describe("slash commands", () => {
|
|||
|
||||
const result = await executeSlashCommand("plan", "OK、実装してください", ctx);
|
||||
|
||||
expect(ctx.toggleCollaborationMode).toHaveBeenCalledOnce();
|
||||
expect(ctx.runtimeSettings.toggleCollaborationMode).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ sendText: "OK、実装してください" });
|
||||
});
|
||||
|
||||
|
|
@ -352,7 +362,7 @@ describe("slash commands", () => {
|
|||
|
||||
const result = await executeSlashCommand("auto-review", "", ctx);
|
||||
|
||||
expect(ctx.toggleAutoReview).toHaveBeenCalledOnce();
|
||||
expect(ctx.runtimeSettings.toggleAutoReview).toHaveBeenCalledOnce();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -361,7 +371,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("auto-review", "この依頼からお願いします", ctx);
|
||||
|
||||
expect(ctx.toggleAutoReview).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.toggleAutoReview).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/auto-review does not take arguments. Usage: /auto-review");
|
||||
});
|
||||
|
||||
|
|
@ -370,7 +380,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("compact", "ignored for now", ctx);
|
||||
|
||||
expect(ctx.compactThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.compactThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/compact does not take arguments. Usage: /compact");
|
||||
});
|
||||
|
||||
|
|
@ -379,7 +389,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("archive", "", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/archive requires a thread. Usage: /archive <thread>");
|
||||
});
|
||||
|
||||
|
|
@ -390,7 +400,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("archive", "thread-beta", ctx);
|
||||
|
||||
expect(ctx.archiveThread).toHaveBeenCalledWith("thread-beta");
|
||||
expect(ctx.threadActions.archiveThread).toHaveBeenCalledWith("thread-beta");
|
||||
});
|
||||
|
||||
it("rejects /archive without a thread before active-thread checks", async () => {
|
||||
|
|
@ -398,7 +408,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("archive", "", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/archive requires a thread. Usage: /archive <thread>");
|
||||
});
|
||||
|
||||
|
|
@ -407,7 +417,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("archive", "thread-1", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Finish or interrupt the current turn before archiving threads.");
|
||||
});
|
||||
|
||||
|
|
@ -418,7 +428,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("archive", "Draft", ctx);
|
||||
|
||||
expect(ctx.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.archiveThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
||||
});
|
||||
|
||||
|
|
@ -429,7 +439,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rename", "thread-beta New Beta Name", ctx);
|
||||
|
||||
expect(ctx.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
||||
expect(ctx.threadActions.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
||||
});
|
||||
|
||||
it("trims /rename names before saving", async () => {
|
||||
|
|
@ -439,7 +449,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rename", "thread-beta New Beta Name ", ctx);
|
||||
|
||||
expect(ctx.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
||||
expect(ctx.threadActions.renameThread).toHaveBeenCalledWith("thread-beta", "New Beta Name");
|
||||
});
|
||||
|
||||
it("rejects /rename without a thread and name", async () => {
|
||||
|
|
@ -447,7 +457,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rename", "thread-1", ctx);
|
||||
|
||||
expect(ctx.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rename requires a thread and a name. Usage: /rename <thread> <name>");
|
||||
});
|
||||
|
||||
|
|
@ -456,7 +466,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rename", "thread-1 ", ctx);
|
||||
|
||||
expect(ctx.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/rename requires a thread and a name. Usage: /rename <thread> <name>");
|
||||
});
|
||||
|
||||
|
|
@ -467,7 +477,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("rename", "Draft New name", ctx);
|
||||
|
||||
expect(ctx.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.threadActions.renameThread).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Multiple matching threads: Draft, Draft notes");
|
||||
});
|
||||
|
||||
|
|
@ -601,22 +611,21 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("reasoning", "extreme", ctx);
|
||||
|
||||
expect(ctx.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.resetReasoningEffortToConfig).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.resetReasoningEffortToConfig).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Unsupported reasoning level: extreme. Usage: /reasoning [level|default]");
|
||||
});
|
||||
|
||||
it("does not announce model or effort changes when applying them fails", async () => {
|
||||
const ctx = context({
|
||||
requestModel: vi.fn().mockResolvedValue(false),
|
||||
requestReasoningEffort: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
const ctx = context();
|
||||
ctx.runtimeSettings.requestModel = vi.fn().mockResolvedValue(false);
|
||||
ctx.runtimeSettings.requestReasoningEffort = vi.fn().mockResolvedValue(false);
|
||||
|
||||
await executeSlashCommand("model", "gpt-5.5", ctx);
|
||||
await executeSlashCommand("reasoning", "high", ctx);
|
||||
|
||||
expect(ctx.requestModel).toHaveBeenCalledWith("gpt-5.5");
|
||||
expect(ctx.requestReasoningEffort).toHaveBeenCalledWith("high");
|
||||
expect(ctx.runtimeSettings.requestModel).toHaveBeenCalledWith("gpt-5.5");
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).toHaveBeenCalledWith("high");
|
||||
expect(ctx.addSystemMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -626,10 +635,10 @@ describe("slash commands", () => {
|
|||
await executeSlashCommand("model", "default", ctx);
|
||||
await executeSlashCommand("reasoning", "default", ctx);
|
||||
|
||||
expect(ctx.resetModelToConfig).toHaveBeenCalledOnce();
|
||||
expect(ctx.resetReasoningEffortToConfig).toHaveBeenCalledOnce();
|
||||
expect(ctx.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.resetModelToConfig).toHaveBeenCalledOnce();
|
||||
expect(ctx.runtimeSettings.resetReasoningEffortToConfig).toHaveBeenCalledOnce();
|
||||
expect(ctx.runtimeSettings.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Model reset to default for subsequent turns.");
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Reasoning effort reset to default for subsequent turns.");
|
||||
});
|
||||
|
|
@ -642,10 +651,10 @@ describe("slash commands", () => {
|
|||
await executeSlashCommand("reasoning", alias, ctx);
|
||||
}
|
||||
|
||||
expect(ctx.resetModelToConfig).toHaveBeenCalledTimes(3);
|
||||
expect(ctx.resetReasoningEffortToConfig).toHaveBeenCalledTimes(3);
|
||||
expect(ctx.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.resetModelToConfig).toHaveBeenCalledTimes(3);
|
||||
expect(ctx.runtimeSettings.resetReasoningEffortToConfig).toHaveBeenCalledTimes(3);
|
||||
expect(ctx.runtimeSettings.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows model and reasoning status for empty runtime commands", async () => {
|
||||
|
|
@ -659,8 +668,8 @@ describe("slash commands", () => {
|
|||
|
||||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Model settings", [{ auditFacts: [{ key: "model", value: "gpt-5.5" }] }]);
|
||||
expect(ctx.addStructuredSystemMessage).toHaveBeenCalledWith("Reasoning effort", [{ auditFacts: [{ key: "effort", value: "high" }] }]);
|
||||
expect(ctx.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.requestModel).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves supported reasoning effort casing", async () => {
|
||||
|
|
@ -670,7 +679,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("reasoning", "CaseSensitive", ctx);
|
||||
|
||||
expect(ctx.requestReasoningEffort).toHaveBeenCalledWith("CaseSensitive");
|
||||
expect(ctx.runtimeSettings.requestReasoningEffort).toHaveBeenCalledWith("CaseSensitive");
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Reasoning effort set to CaseSensitive for subsequent turns.");
|
||||
});
|
||||
|
||||
|
|
@ -697,7 +706,7 @@ describe("slash commands", () => {
|
|||
|
||||
await executeSlashCommand("fast", "now", ctx);
|
||||
|
||||
expect(ctx.toggleFastMode).not.toHaveBeenCalled();
|
||||
expect(ctx.runtimeSettings.toggleFastMode).not.toHaveBeenCalled();
|
||||
expect(ctx.addSystemMessage).toHaveBeenCalledWith("/fast does not take arguments. Usage: /fast");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -38,23 +38,29 @@ function createHost(overrides: SlashCommandHostOverrides = {}) {
|
|||
startNewThread: vi.fn().mockResolvedValue(undefined),
|
||||
startThreadForGoal: vi.fn().mockResolvedValue("thread-new"),
|
||||
resumeThread: vi.fn().mockResolvedValue(undefined),
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread,
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
renameThread: vi.fn().mockResolvedValue(undefined),
|
||||
threadActions: {
|
||||
forkThread: vi.fn().mockResolvedValue(undefined),
|
||||
rollbackThread: vi.fn().mockResolvedValue(undefined),
|
||||
compactThread,
|
||||
archiveThread: vi.fn().mockResolvedValue(undefined),
|
||||
renameThread: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
reconnect: vi.fn().mockResolvedValue(undefined),
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
requestModel: vi.fn(),
|
||||
resetModelToConfig: vi.fn(),
|
||||
requestReasoningEffort: vi.fn(),
|
||||
resetReasoningEffortToConfig: vi.fn(),
|
||||
activeGoal: vi.fn(() => stateStore.getState().activeThread.goal),
|
||||
setGoalObjective: vi.fn().mockResolvedValue(true),
|
||||
setGoalStatus: vi.fn().mockResolvedValue(true),
|
||||
clearGoal: vi.fn().mockResolvedValue(true),
|
||||
runtimeSettings: {
|
||||
toggleFastMode: vi.fn(),
|
||||
toggleCollaborationMode: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
requestModel: vi.fn(),
|
||||
resetModelToConfig: vi.fn(),
|
||||
requestReasoningEffort: vi.fn(),
|
||||
resetReasoningEffortToConfig: vi.fn(),
|
||||
},
|
||||
goals: {
|
||||
activeGoal: vi.fn(() => stateStore.getState().activeThread.goal),
|
||||
setObjective: vi.fn().mockResolvedValue(true),
|
||||
setStatus: vi.fn().mockResolvedValue(true),
|
||||
clear: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
addSystemMessage: vi.fn(),
|
||||
addStructuredSystemMessage: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
|
|
@ -126,7 +132,7 @@ describe("executeSlashCommandWithState", () => {
|
|||
await executeSlashCommandWithState(host, "goal", "set Ship this");
|
||||
|
||||
expect(host.startThreadForGoal).toHaveBeenCalledWith("Ship this");
|
||||
expect(host.setGoalObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
expect(host.goals.setObjective).toHaveBeenCalledWith("thread-new", "Ship this", null);
|
||||
});
|
||||
|
||||
it("runs reconnect even when there is no current app-server client", async () => {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
|
||||
import { ThreadRenameEditorController } from "../../../../src/features/chat/application/threads/rename-editor-controller";
|
||||
import type { AppServerClient } from "../../../../src/app-server/connection/client";
|
||||
import type { Thread } from "../../../../src/domain/threads/model";
|
||||
import { normalizeExplicitThreadName, type Thread } from "../../../../src/domain/threads/model";
|
||||
import type { TurnItem, TurnRecord } from "../../../../src/app-server/protocol/turn";
|
||||
import { threadRenameFromValue } from "../../../../src/app-server/services/thread-rename";
|
||||
import { deferred } from "../../../support/async";
|
||||
|
||||
describe("ThreadRenameEditorController", () => {
|
||||
|
|
@ -163,16 +162,14 @@ function controllerFixture(
|
|||
addSystemMessage: overrides.addSystemMessage ?? vi.fn(),
|
||||
operations: {
|
||||
renameThread: async (threadId: string, value: string) => {
|
||||
const rename = threadRenameFromValue(value);
|
||||
if (!rename) return false;
|
||||
await currentClient().setThreadName(threadId, rename.name);
|
||||
const name = normalizeExplicitThreadName(value);
|
||||
if (!name) return false;
|
||||
await currentClient().setThreadName(threadId, name);
|
||||
stateStore.dispatch({
|
||||
type: "thread-list/applied",
|
||||
threads: stateStore
|
||||
.getState()
|
||||
.threadList.listedThreads.map((thread) => (thread.id === threadId ? { ...thread, name: rename.name } : thread)),
|
||||
threads: stateStore.getState().threadList.listedThreads.map((thread) => (thread.id === threadId ? { ...thread, name } : thread)),
|
||||
});
|
||||
notifyThreadRenamed(threadId, rename.name);
|
||||
notifyThreadRenamed(threadId, name);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { AppServerClient } from "../../../../src/app-server/connection/clie
|
|||
import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
|
||||
import { archiveThreadOnAppServer } from "../../../../src/app-server/services/thread-archive";
|
||||
import type { ArchiveExportAdapter } from "../../../../src/app-server/services/thread-archive-markdown";
|
||||
import { threadRenameFromValue } from "../../../../src/app-server/services/thread-rename";
|
||||
import { normalizeExplicitThreadName } from "../../../../src/domain/threads/model";
|
||||
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
|
||||
import {
|
||||
createThreadManagementActions,
|
||||
|
|
@ -451,11 +451,11 @@ function hostMock({
|
|||
return result;
|
||||
}),
|
||||
renameThread: vi.fn(async (threadId: string, value: string) => {
|
||||
const rename = threadRenameFromValue(value);
|
||||
if (!rename) return false;
|
||||
const name = normalizeExplicitThreadName(value);
|
||||
if (!name) return false;
|
||||
await ensureConnected();
|
||||
await client.setThreadName(threadId, rename.name);
|
||||
notifyThreadRenamed(threadId, rename.name);
|
||||
await client.setThreadName(threadId, name);
|
||||
notifyThreadRenamed(threadId, name);
|
||||
return true;
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -505,28 +505,6 @@ describe("CodexPanelPlugin boot restored panel loading", () => {
|
|||
expect(threadCatalog(plugin).activeThreadsSnapshot()).toEqual([thread("cached")]);
|
||||
});
|
||||
|
||||
it("refreshes shared thread lists from a connected chat panel", async () => {
|
||||
const { CodexChatView } = await import("../src/features/chat/host/view");
|
||||
const disconnectedLeaf = leaf();
|
||||
disconnectedLeaf.view = chatView(CodexChatView, disconnectedLeaf);
|
||||
const disconnectedView = disconnectedLeaf.view as CodexChatView;
|
||||
vi.spyOn(disconnectedView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "disconnected", connected: false }));
|
||||
const disconnectedRefresh = vi.spyOn(disconnectedView.surface, "refreshSharedThreadList").mockResolvedValue(undefined);
|
||||
|
||||
const connectedLeaf = leaf();
|
||||
connectedLeaf.view = chatView(CodexChatView, connectedLeaf);
|
||||
const connectedView = connectedLeaf.view as CodexChatView;
|
||||
vi.spyOn(connectedView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "connected", connected: true }));
|
||||
const connectedRefresh = vi.spyOn(connectedView.surface, "refreshSharedThreadList").mockResolvedValue(undefined);
|
||||
|
||||
const plugin = await pluginWithLeaves([disconnectedLeaf, connectedLeaf]);
|
||||
|
||||
plugin.runtime.settingTabHost().threadCatalog.invalidateThreadsFromOpenSurface();
|
||||
|
||||
expect(disconnectedRefresh).not.toHaveBeenCalled();
|
||||
expect(connectedRefresh).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("refreshes shared thread lists from a remaining connected panel after the archived panel is detached", async () => {
|
||||
const { CodexChatView } = await import("../src/features/chat/host/view");
|
||||
const sourceLeaf = leaf();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { CatalogHookMetadata, CatalogModel } from "../../src/app-server/pro
|
|||
import type { ThreadRecord } from "../../src/app-server/protocol/thread";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../src/domain/catalog/metadata";
|
||||
import { modelMetadataFromCatalogModels } from "../../src/app-server/protocol/catalog";
|
||||
import { SettingsDynamicDataController } from "../../src/settings/dynamic-data-controller";
|
||||
import { SettingsDynamicDataController, type SettingsDynamicDataSnapshot } from "../../src/settings/dynamic-data-controller";
|
||||
import { CodexPanelSettingTab } from "../../src/settings/tab";
|
||||
import type { CodexPanelSettingTabHost } from "../../src/settings/tab";
|
||||
import type { Thread } from "../../src/domain/threads/model";
|
||||
|
|
@ -357,9 +357,49 @@ describe("settings tab", () => {
|
|||
expect(controller.snapshot().hooks.map((item) => item.currentHash)).toEqual(["newhash"]);
|
||||
});
|
||||
|
||||
it("keeps full refresh models and archived threads current when hook operations overlap", async () => {
|
||||
const models = deferred<readonly ModelMetadata[]>();
|
||||
const fullRefreshClient = settingsClient({
|
||||
hooks: [hook({ key: "hook-full", command: "full refresh hook", currentHash: "fullhash" })],
|
||||
threads: [appServerThread({ id: "thread-full", preview: "Full archived" })],
|
||||
});
|
||||
const trustClient = {
|
||||
trustHook: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
const hookReloadClient = settingsClient({
|
||||
hooks: [hook({ key: "hook-new", command: "new hook", currentHash: "newhash" })],
|
||||
});
|
||||
withShortLivedAppServerClientMock
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(fullRefreshClient),
|
||||
)
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(trustClient),
|
||||
)
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(hookReloadClient),
|
||||
);
|
||||
const refreshModels = vi.fn().mockReturnValue(models.promise);
|
||||
const controller = new SettingsDynamicDataController(settingsTabHost({ refreshModels }), { display: vi.fn(), notify: vi.fn() });
|
||||
|
||||
const fullRefresh = controller.refreshSettingsData();
|
||||
await flushPromises();
|
||||
await controller.trustHook(hook({ key: "hook-initial", command: "initial hook", currentHash: "initialhash" }));
|
||||
|
||||
models.resolve(modelMetadataFromCatalogModels([model("gpt-refreshed")]));
|
||||
await fullRefresh;
|
||||
|
||||
const snapshot = controller.snapshot();
|
||||
expect(snapshot.models.map((item) => item.model)).toEqual(["gpt-refreshed"]);
|
||||
expect(snapshot.modelsLifecycle.kind).toBe("loaded");
|
||||
expect(snapshot.archivedThreads.map((thread) => thread.preview)).toEqual(["Full archived"]);
|
||||
expect(snapshot.archivedThreadsLifecycle.kind).toBe("loaded");
|
||||
expect(snapshot.hooks.map((item) => item.currentHash)).toEqual(["newhash"]);
|
||||
});
|
||||
|
||||
it("ignores stale archived restore results after a newer dynamic operation completes", async () => {
|
||||
const staleRestore = deferred<{ thread: ThreadRecord }>();
|
||||
const invalidateThreadsFromOpenSurface = vi.fn();
|
||||
const refreshActiveThreads = vi.fn().mockResolvedValue([]);
|
||||
const initialClient = settingsClient({
|
||||
threads: [appServerThread({ id: "thread-old", preview: "Old archived" })],
|
||||
});
|
||||
|
|
@ -379,7 +419,7 @@ describe("settings tab", () => {
|
|||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(newerClient),
|
||||
);
|
||||
const controller = new SettingsDynamicDataController(settingsTabHost({ invalidateThreadsFromOpenSurface }), {
|
||||
const controller = new SettingsDynamicDataController(settingsTabHost({ refreshActiveThreads }), {
|
||||
display: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
});
|
||||
|
|
@ -394,10 +434,77 @@ describe("settings tab", () => {
|
|||
staleRestore.resolve({ thread: appServerThread({ id: "thread-old", preview: "Restored old" }) });
|
||||
await restore;
|
||||
|
||||
expect(invalidateThreadsFromOpenSurface).not.toHaveBeenCalled();
|
||||
expect(refreshActiveThreads).not.toHaveBeenCalled();
|
||||
expect(controller.snapshot().archivedThreads.map((thread) => thread.preview)).toEqual(["New archived"]);
|
||||
});
|
||||
|
||||
it("keeps restored archived threads removed when active thread refresh fails", async () => {
|
||||
const notify = vi.fn();
|
||||
const refreshActiveThreads = vi.fn().mockRejectedValue(new Error("offline"));
|
||||
const initialClient = settingsClient({
|
||||
threads: [appServerThread({ id: "thread-old", preview: "Old archived" })],
|
||||
});
|
||||
const restoreClient = {
|
||||
unarchiveThread: vi.fn().mockResolvedValue({ thread: appServerThread({ id: "thread-old", preview: "Restored old" }) }),
|
||||
};
|
||||
withShortLivedAppServerClientMock
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(initialClient),
|
||||
)
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(restoreClient),
|
||||
);
|
||||
const controller = new SettingsDynamicDataController(settingsTabHost({ refreshActiveThreads }), { display: vi.fn(), notify });
|
||||
|
||||
await controller.refreshSettingsData();
|
||||
await controller.restoreArchivedThread("thread-old");
|
||||
|
||||
const snapshot = controller.snapshot();
|
||||
expect(snapshot.archivedThreads).toEqual([]);
|
||||
expect(snapshot.archivedThreadsLifecycle.kind).toBe("loaded");
|
||||
expect(refreshActiveThreads).toHaveBeenCalledOnce();
|
||||
expect(notify).toHaveBeenCalledWith("Could not refresh active Codex threads.");
|
||||
expect(notify).not.toHaveBeenCalledWith("Could not restore archived Codex thread.");
|
||||
});
|
||||
|
||||
it("displays restored archived thread state before active thread refresh completes", async () => {
|
||||
const activeRefresh = deferred<readonly Thread[]>();
|
||||
const snapshots: SettingsDynamicDataSnapshot[] = [];
|
||||
const initialClient = settingsClient({
|
||||
threads: [appServerThread({ id: "thread-old", preview: "Old archived" })],
|
||||
});
|
||||
const restoreClient = {
|
||||
unarchiveThread: vi.fn().mockResolvedValue({ thread: appServerThread({ id: "thread-old", preview: "Restored old" }) }),
|
||||
};
|
||||
withShortLivedAppServerClientMock
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(initialClient),
|
||||
)
|
||||
.mockImplementationOnce((_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) =>
|
||||
operation(restoreClient),
|
||||
);
|
||||
const controllerRef: { current: SettingsDynamicDataController | null } = { current: null };
|
||||
const controller = new SettingsDynamicDataController(settingsTabHost({ refreshActiveThreads: () => activeRefresh.promise }), {
|
||||
display: () => {
|
||||
const snapshot = controllerRef.current?.snapshot();
|
||||
if (snapshot) snapshots.push(snapshot);
|
||||
},
|
||||
notify: vi.fn(),
|
||||
});
|
||||
controllerRef.current = controller;
|
||||
|
||||
await controller.refreshSettingsData();
|
||||
snapshots.length = 0;
|
||||
const restore = controller.restoreArchivedThread("thread-old");
|
||||
await flushPromises();
|
||||
|
||||
expect(snapshots.at(-1)?.archivedThreads).toEqual([]);
|
||||
expect(snapshots.at(-1)?.archivedThreadsLifecycle.kind).toBe("loaded");
|
||||
|
||||
activeRefresh.resolve([]);
|
||||
await restore;
|
||||
});
|
||||
|
||||
it("uses cached models initially and publishes refreshed models", async () => {
|
||||
const fetchModels = vi.fn().mockResolvedValue(modelMetadataFromCatalogModels([model("gpt-5.5")]));
|
||||
const client = settingsClient({ models: [model("gpt-5.5")] });
|
||||
|
|
@ -572,14 +679,13 @@ describe("settings tab", () => {
|
|||
});
|
||||
|
||||
it("permanently deletes an archived thread from the confirmed settings row", async () => {
|
||||
const invalidateThreadsFromOpenSurface = vi.fn();
|
||||
const client = settingsClient({
|
||||
threads: [appServerThread({ id: "thread-archived", preview: "Archived thread" })],
|
||||
});
|
||||
withShortLivedAppServerClientMock.mockImplementation(
|
||||
(_codexPath: string, _cwd: string, operation: (client: unknown) => Promise<unknown>) => operation(client),
|
||||
);
|
||||
const tab = newSettingsTab({ invalidateThreadsFromOpenSurface });
|
||||
const tab = newSettingsTab();
|
||||
|
||||
tab.display();
|
||||
await flushPromises();
|
||||
|
|
@ -590,7 +696,6 @@ describe("settings tab", () => {
|
|||
await flushPromises();
|
||||
|
||||
expect(client.deleteThread).toHaveBeenCalledWith("thread-archived");
|
||||
expect(invalidateThreadsFromOpenSurface).toHaveBeenCalledOnce();
|
||||
expect(tab.containerEl.textContent).toContain("No archived threads.");
|
||||
expect(tab.containerEl.querySelectorAll(".codex-panel-settings__archived-list .setting-item")).toHaveLength(0);
|
||||
});
|
||||
|
|
@ -709,7 +814,7 @@ function newSettingsTab(
|
|||
observeModels?: CodexPanelSettingTabHost["appServerData"]["observeModelsResult"];
|
||||
notifyContextChanged?: () => void;
|
||||
refreshOpenViews?: () => void;
|
||||
invalidateThreadsFromOpenSurface?: () => void;
|
||||
refreshActiveThreads?: () => Promise<readonly Thread[]>;
|
||||
settings?: Partial<{
|
||||
threadNamingModel: string | null;
|
||||
threadNamingEffort: string | null;
|
||||
|
|
@ -731,7 +836,7 @@ function settingsTabHost(
|
|||
observeModels?: CodexPanelSettingTabHost["appServerData"]["observeModelsResult"];
|
||||
notifyContextChanged?: () => void;
|
||||
refreshOpenViews?: () => void;
|
||||
invalidateThreadsFromOpenSurface?: () => void;
|
||||
refreshActiveThreads?: () => Promise<readonly Thread[]>;
|
||||
settings?: Partial<{
|
||||
threadNamingModel: string | null;
|
||||
threadNamingEffort: string | null;
|
||||
|
|
@ -766,7 +871,7 @@ function settingsTabHost(
|
|||
notifyContextChanged: options.notifyContextChanged ?? vi.fn(),
|
||||
},
|
||||
threadCatalog: {
|
||||
invalidateThreadsFromOpenSurface: options.invalidateThreadsFromOpenSurface ?? vi.fn(),
|
||||
refreshActiveThreads: options.refreshActiveThreads ?? vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { createSharedThreadCatalog } from "../../src/workspace/shared-thread-cat
|
|||
|
||||
interface MockSurfaceActions {
|
||||
refreshOpenViews: Mock<() => void>;
|
||||
invalidateThreadsFromOpenSurface: Mock<() => void>;
|
||||
applyThreadArchived: Mock<(threadId: string, options?: { closeOpenPanels?: boolean }) => void>;
|
||||
applyThreadRenamed: Mock<(threadId: string, name: string | null) => void>;
|
||||
refreshThreadsViewLiveState: Mock<() => void>;
|
||||
|
|
@ -107,7 +106,6 @@ function cacheWithThreads(
|
|||
function surfaceActions(): MockSurfaceActions {
|
||||
return {
|
||||
refreshOpenViews: vi.fn(),
|
||||
invalidateThreadsFromOpenSurface: vi.fn(),
|
||||
applyThreadArchived: vi.fn(),
|
||||
applyThreadRenamed: vi.fn(),
|
||||
refreshThreadsViewLiveState: vi.fn(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue