Inline low-value source helpers

This commit is contained in:
murashit 2026-06-27 13:39:14 +09:00
parent 7a9f179020
commit 582bc0195d
49 changed files with 194 additions and 574 deletions

View file

@ -95,7 +95,7 @@ function hookItemFromCatalogHook(hook: CatalogHookMetadata): HookItem {
enabled: hook.enabled,
isManaged: hook.isManaged,
currentHash: hook.currentHash,
trustStatus: hookTrustStatusFromCatalogTrustStatus(hook.trustStatus),
trustStatus: hook.trustStatus,
};
}
@ -107,14 +107,6 @@ export function appServerHookOperationFromHookItem(hook: HookItem): AppServerHoo
return {
key: hook.key,
currentHash: hook.currentHash,
trustStatus: appServerHookTrustStatus(hook.trustStatus),
trustStatus: hook.trustStatus,
};
}
function hookTrustStatusFromCatalogTrustStatus(status: AppServerHookTrustStatus): HookItem["trustStatus"] {
return status;
}
function appServerHookTrustStatus(status: HookItem["trustStatus"]): AppServerHookTrustStatus {
return status;
}

View file

@ -50,11 +50,5 @@ function appServerUserInputItemFromCodexInputItem(item: CodexInputItem): AppServ
function appServerImageDetailProp(detail: Extract<CodexInputItem, { type: "image" | "localImage" }>["detail"]): {
detail?: AppServerUserInputImageDetail;
} {
return detail === undefined ? {} : { detail: appServerUserInputImageDetail(detail) };
}
function appServerUserInputImageDetail(
detail: NonNullable<Extract<CodexInputItem, { type: "image" | "localImage" }>["detail"]>,
): AppServerUserInputImageDetail {
return detail;
return detail === undefined ? {} : { detail };
}

View file

@ -16,7 +16,8 @@ export interface ConfigReadResult {
}
export function runtimeConfigSnapshotFromAppServerConfig(response: ConfigReadResult): RuntimeConfigSnapshot {
const config = asRecord(response.config);
const rawConfig = response.config;
const config = rawConfig && typeof rawConfig === "object" ? (rawConfig as Record<string, unknown>) : {};
const effort = config["model_reasoning_effort"];
return {
profile: selectedConfigProfile(response.layers),
@ -32,10 +33,6 @@ export function runtimeConfigSnapshotFromAppServerConfig(response: ConfigReadRes
};
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function selectedConfigProfile(layers: ConfigReadResult["layers"]): string | null {
let selected: string | null = null;
if (!layers) return null;

View file

@ -39,19 +39,15 @@ function rateLimitSnapshotFromAppServerSnapshot(snapshot: AppServerRateLimitSnap
}
export function rateLimitSnapshotFromAccountRateLimitsResponse(response: AppServerAccountRateLimitsResponse): RateLimitSnapshot {
return rateLimitSnapshotFromAppServerSnapshot(accountRateLimitSnapshotFromResponse(response));
const snapshots = response.rateLimitsByLimitId;
const snapshot = (snapshots && Object.hasOwn(snapshots, "codex") ? snapshots["codex"] : undefined) ?? response.rateLimits;
return rateLimitSnapshotFromAppServerSnapshot(snapshot);
}
export function accountRateLimitsSummaryFromResponse(response: AppServerAccountRateLimitsResponse): string {
return response.rateLimitsByLimitId ? `${String(Object.keys(response.rateLimitsByLimitId).length)} limits` : "available";
}
function accountRateLimitSnapshotFromResponse(response: AppServerAccountRateLimitsResponse): AppServerRateLimitSnapshot {
const snapshots = response.rateLimitsByLimitId;
const codexRateLimit = snapshots && Object.hasOwn(snapshots, "codex") ? snapshots["codex"] : undefined;
return codexRateLimit ?? response.rateLimits;
}
function rateLimitWindowFromAppServerWindow(window: AppServerRateLimitWindow): RateLimitWindow {
return {
usedPercent: window.usedPercent,

View file

@ -748,14 +748,7 @@ function bigintToNumberOrNull(value: unknown): number | null {
function toJsonContent(content: Record<string, McpElicitationContentValue> | null): unknown {
if (!content) return null;
return Object.fromEntries(Object.entries(content).map(([key, value]) => [key, toJsonValue(value)]));
}
function toJsonValue(value: McpElicitationContentValue): unknown {
if (isReadonlyStringArray(value)) return [...value];
return value;
}
function isReadonlyStringArray(value: McpElicitationContentValue): value is readonly string[] {
return Array.isArray(value);
return Object.fromEntries(
Object.entries(content).map(([key, value]) => [key, Array.isArray(value) ? Array.from(value as readonly string[]) : value]),
);
}

View file

@ -19,7 +19,7 @@ export function threadGoalFromAppServerGoal(goal: AppServerThreadGoal | null): T
return {
threadId: goal.threadId,
objective: goal.objective,
status: threadGoalStatusFromAppServerStatus(goal.status),
status: goal.status,
tokenBudget: goal.tokenBudget,
tokensUsed: finiteNumber(goal.tokensUsed),
timeUsedSeconds: finiteNumber(goal.timeUsedSeconds),
@ -35,7 +35,7 @@ export function appServerThreadGoalUpdate(update: ThreadGoalUpdate): {
} {
return {
...("objective" in update ? { objective: update.objective } : {}),
...("status" in update ? { status: update.status === null ? null : appServerThreadGoalStatus(update.status) } : {}),
...("status" in update ? { status: update.status === null ? null : update.status } : {}),
...("tokenBudget" in update ? { tokenBudget: update.tokenBudget } : {}),
};
}
@ -48,14 +48,6 @@ export function appServerThreadGoalUserHistoryItem(text: string): AppServerJsonV
};
}
function threadGoalStatusFromAppServerStatus(status: AppServerThreadGoalStatus): ThreadGoalStatus {
return status;
}
function appServerThreadGoalStatus(status: ThreadGoalStatus): AppServerThreadGoalStatus {
return status;
}
function finiteNumber(value: number): number {
return Number.isFinite(value) ? value : 0;
}

View file

@ -11,14 +11,16 @@ export interface ThreadRecord {
}
export function threadFromThreadRecord(thread: ThreadRecord, options: { archived?: boolean } = {}): Thread {
const hasRecencyAt = Object.hasOwn(thread, "recencyAt");
const recencyAt = hasRecencyAt ? thread.recencyAt : undefined;
return {
id: thread.id,
preview: normalizeString(thread.preview),
name: normalizeNullableString(thread.name),
name: thread.name === null ? null : normalizeString(thread.name),
archived: options.archived ?? false,
createdAt: finiteTimestamp(thread.createdAt),
updatedAt: finiteTimestamp(thread.updatedAt),
...recencyAtPatch(thread),
...(hasRecencyAt ? { recencyAt: typeof recencyAt === "number" && Number.isFinite(recencyAt) ? recencyAt : null } : {}),
};
}
@ -26,10 +28,6 @@ export function threadsFromThreadRecords(threads: readonly ThreadRecord[], optio
return threads.map((thread) => threadFromThreadRecord(thread, options));
}
function normalizeNullableString(value: string | null): string | null {
return value === null ? null : normalizeString(value);
}
function normalizeString(value: string): string {
return typeof value === "string" ? value : "";
}
@ -37,9 +35,3 @@ function normalizeString(value: string): string {
function finiteTimestamp(value: number): number {
return Number.isFinite(value) ? value : 0;
}
function recencyAtPatch(thread: ThreadRecord): { recencyAt: number | null } | Record<string, never> {
if (!Object.hasOwn(thread, "recencyAt")) return {};
const value = thread.recencyAt;
return { recencyAt: typeof value === "number" && Number.isFinite(value) ? value : null };
}

View file

@ -154,12 +154,10 @@ export function completedConversationSummariesFromTurnRecords(turns: readonly Tu
});
}
function conversationSummariesFromTurnRecords(turns: readonly TurnRecord[]): ThreadConversationSummary[] {
return nonEmptyConversationSummaries(turns.map(conversationSummaryFromTurnRecord));
}
export function chronologicalConversationSummariesFromTurnRecords(turns: readonly TurnRecord[]): ThreadConversationSummary[] {
return conversationSummariesFromTurnRecords(chronologicalTurnRecords(turns));
return nonEmptyConversationSummaries(
[...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0)).map(conversationSummaryFromTurnRecord),
);
}
export function turnUserItemText(item: Extract<TurnItem, { type: "userMessage" }>): string {
@ -206,7 +204,3 @@ function userInputText(content: readonly AppServerUserInput[]): string {
.filter(Boolean)
.join("\n");
}
function chronologicalTurnRecords(turns: readonly TurnRecord[]): TurnRecord[] {
return [...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
}

View file

@ -18,7 +18,9 @@ export async function readSkillMetadataProbe(
vaultPath: string,
forceReload = false,
): Promise<SkillMetadataProbeResult> {
if (!client) return disconnectedSkillsResult();
if (!client) {
return { value: [], probe: diagnosticProbeError("skills/list", new Error("Codex app-server is not connected."), Date.now()) };
}
try {
const catalog = await listSkillCatalog(client, vaultPath, { forceReload });
return { value: catalog.skills, probe: diagnosticProbeOk("skills/list", `${String(catalog.totalCount)} skills`, Date.now()) };
@ -28,7 +30,12 @@ export async function readSkillMetadataProbe(
}
export async function readRateLimitMetadataProbe(client: AppServerClient | null): Promise<RateLimitMetadataProbeResult> {
if (!client) return disconnectedRateLimitResult();
if (!client) {
return {
value: null,
probe: diagnosticProbeError("account/rateLimits/read", new Error("Codex app-server is not connected."), Date.now()),
};
}
try {
const response = await client.readAccountRateLimits();
return {
@ -39,14 +46,3 @@ export async function readRateLimitMetadataProbe(client: AppServerClient | null)
return { value: null, probe: diagnosticProbeError("account/rateLimits/read", error, Date.now()) };
}
}
function disconnectedSkillsResult(): SkillMetadataProbeResult {
return { value: [], probe: diagnosticProbeError("skills/list", new Error("Codex app-server is not connected."), Date.now()) };
}
function disconnectedRateLimitResult(): RateLimitMetadataProbeResult {
return {
value: null,
probe: diagnosticProbeError("account/rateLimits/read", new Error("Codex app-server is not connected."), Date.now()),
};
}

View file

@ -1,4 +1,4 @@
import type { ModelMetadata, SkillMetadata } from "../../domain/catalog/metadata";
import type { ModelMetadata } from "../../domain/catalog/metadata";
import { cloneRuntimeConfigSnapshot } from "../../domain/runtime/config";
import type { RateLimitSnapshot } from "../../domain/runtime/metrics";
import type { SharedServerMetadata } from "../../domain/server/metadata";
@ -25,7 +25,7 @@ export function cloneSharedServerMetadata(metadata: SharedServerMetadata): Share
runtimeConfig: metadata.runtimeConfig ? cloneRuntimeConfigSnapshot(metadata.runtimeConfig) : null,
rateLimit: metadata.rateLimit ? cloneRateLimitSnapshot(metadata.rateLimit) : null,
availableModels: cloneModelMetadata(metadata.availableModels),
availableSkills: cloneSkillMetadata(metadata.availableSkills),
availableSkills: metadata.availableSkills.map((skill) => ({ ...skill })),
serverDiagnostics: {
probes: { ...metadata.serverDiagnostics.probes },
mcpServers: metadata.serverDiagnostics.mcpServers.map((server) => ({ ...server })),
@ -42,7 +42,3 @@ function cloneRateLimitSnapshot(snapshot: RateLimitSnapshot): RateLimitSnapshot
individualLimit: snapshot.individualLimit ? { ...snapshot.individualLimit } : null,
};
}
function cloneSkillMetadata(skills: readonly SkillMetadata[]): SkillMetadata[] {
return skills.map((skill) => ({ ...skill }));
}

View file

@ -22,18 +22,15 @@ export function isTurnScopedMessageForIdleActiveThread(messageScope: MessageScop
}
export function fallbackMessageScope(message: { params?: unknown }): MessageScope {
const params = messageParams(message);
const rawParams = message.params;
const params =
rawParams !== null && typeof rawParams === "object" && !Array.isArray(rawParams) ? (rawParams as Record<string, unknown>) : null;
return {
threadId: stringParam(params, "threadId"),
turnId: stringParam(params, "turnId"),
};
}
function messageParams(message: { params?: unknown }): Record<string, unknown> | null {
const params = message.params;
return params !== null && typeof params === "object" && !Array.isArray(params) ? (params as Record<string, unknown>) : null;
}
function stringParam(params: Record<string, unknown> | null, key: string): string | null {
const value = params?.[key];
return typeof value === "string" && value.length > 0 ? value : null;

View file

@ -5,8 +5,7 @@ import {
threadTitleFromGeneratedText,
threadTitlePrompt,
} from "../../domain/threads/title-generation-model";
import type { ModelMetadataClient } from "../catalog";
import { conversationAssistantTextFromTurnRecord, type TurnRecord } from "../protocol/turn";
import { conversationAssistantTextFromTurnRecord } from "../protocol/turn";
import {
type EphemeralStructuredTurnClientFactory,
runEphemeralStructuredTurn,
@ -60,17 +59,10 @@ export async function generateThreadTitleWithCodex(
serverRequests: { kind: "reject", message: "Thread title generation does not handle server requests." },
exitedMessage: "Codex title generation app-server exited.",
timedOutMessage: "Timed out while generating a Codex thread title.",
resolveRuntime: (client) => threadTitleRuntimeOverrideForClient(client, runtimeSettings),
resolveRuntime: (client) =>
resolvedRuntimeOverrideForClient(client, { model: runtimeSettings.threadNamingModel, effort: runtimeSettings.threadNamingEffort }),
clientFactory,
});
return threadTitleFromGenerationTurn(turn);
}
function threadTitleFromGenerationTurn(turn: TurnRecord): string | null {
const response = conversationAssistantTextFromTurnRecord(turn);
return response ? threadTitleFromGeneratedText(response) : null;
}
async function threadTitleRuntimeOverrideForClient(client: ModelMetadataClient, settings: ThreadTitleRuntimeSettings) {
return resolvedRuntimeOverrideForClient(client, { model: settings.threadNamingModel, effort: settings.threadNamingEffort });
}

View file

@ -44,12 +44,8 @@ export interface HookItem {
export type ReasoningEffort = string;
function nonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function normalizeReasoningEffort(value: unknown): ReasoningEffort | null {
return nonEmptyString(value) ? value.trim() : null;
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
export function supportedEffortsForModelMetadata(model: ModelMetadata | null): ReasoningEffort[] {

View file

@ -13,7 +13,7 @@ export function explicitThreadName(thread: Thread): string | null {
}
export function normalizeExplicitThreadName(value: string | null | undefined): string | null {
const name = typeof value === "string" ? normalizeTitle(value) : "";
const name = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
return name.length > 0 ? name : null;
}
@ -31,7 +31,3 @@ export function upsertThread(threads: readonly Thread[], thread: Thread): Thread
export function threadRecencyAt(thread: Thread): number {
return thread.recencyAt ?? thread.updatedAt;
}
function normalizeTitle(value: string): string {
return value.replace(/\s+/g, " ").trim();
}

View file

@ -26,7 +26,9 @@ export function threadArchiveTitle(thread: Thread): string {
}
export function threadArchiveDisplayTitle(thread: Thread): string {
return truncateThreadTitle(threadArchiveTitle(thread), MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH);
const title = threadArchiveTitle(thread);
if (title.length <= MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH) return title;
return `${title.slice(0, MAX_ARCHIVED_THREAD_DISPLAY_TITLE_LENGTH - 3).trimEnd()}...`;
}
export function threadWindowTitle(activeThreadId: string | null, threads: readonly Thread[], fallbackTitle?: string | null): string {
@ -45,8 +47,3 @@ export function threadWindowTitle(activeThreadId: string | null, threads: readon
function normalizeThreadTitleText(value: string | null | undefined): string {
return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
}
function truncateThreadTitle(value: string, maxLength: number): string {
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength - 3).trimEnd()}...`;
}

View file

@ -10,8 +10,6 @@ import {
type ApprovalAction,
contentForPendingMcpElicitation,
type McpElicitationAction,
type PendingApproval,
type PendingMcpElicitation,
type PendingRequestId,
type PendingUserInput,
} from "../../../../domain/pending-requests/model";
@ -32,34 +30,6 @@ import type { AppServerResourceEvent } from "../actions/metadata";
import { classifyAppServerLog } from "./app-server-logs";
import { type ChatNotificationEffect, planChatNotification } from "./notification-plan";
function cannotSendApprovalResponseMessage(): string {
return "Could not send approval response because Codex app-server is not connected.";
}
function cannotSendUserInputMessage(): string {
return "Could not send user input because Codex app-server is not connected.";
}
function cannotCancelUserInputMessage(): string {
return "Could not cancel user input because Codex app-server is not connected.";
}
function cannotSendMcpElicitationMessage(): string {
return "Could not send MCP request response because Codex app-server is not connected.";
}
function cannotSendCurrentTimeMessage(): string {
return "Could not send current time because Codex app-server is not connected.";
}
function userCancelledInputRequestMessage(): string {
return "User cancelled input request.";
}
function cannotRejectServerRequestMessage(): string {
return "Could not reject app-server request because Codex app-server is not connected.";
}
export interface ChatInboundHandlerActions {
refreshActiveThreads: () => void;
refreshServerDiagnostics: (options?: { forceResourceProbes?: boolean }) => void;
@ -144,29 +114,34 @@ function handleNotification(context: ChatInboundHandlerContext, notification: Se
}
function handleServerRequest(context: ChatInboundHandlerContext, request: ServerRequest): void {
const route = routeServerRequest(request, activeRouteScope(context));
const current = state(context);
const route = routeServerRequest(request, { activeThreadId: current.activeThread.id, activeTurnId: activeTurnId(current) });
switch (route.kind) {
case "approval":
queueApprovalRequest(context, route.approval);
dispatch(context, { type: "request/approval-queued", approval: route.approval });
return;
case "userInput":
queueUserInputRequest(context, route.input);
dispatch(context, { type: "request/user-input-queued", input: route.input });
return;
case "mcpElicitation":
queueMcpElicitationRequest(context, route.elicitation);
dispatch(context, { type: "request/mcp-elicitation-queued", elicitation: route.elicitation });
return;
case "currentTime":
respondToCurrentTimeRequest(context, route.request);
if (!context.actions.respondToServerRequest(route.request.id, serverRequestCurrentTimeResponse(Date.now()))) {
addSystemMessage(context, "Could not send current time because Codex app-server is not connected.");
}
return;
case "inactive":
rejectServerRequest(context, request, `Rejected inactive app-server request: ${request.method}`);
return;
case "unsupported":
rejectUnsupportedServerRequest(context, request);
rejectServerRequest(context, request, `Rejected unsupported app-server request: ${request.method}`);
return;
case "unknown":
rejectUnknownServerRequest(context, request);
case "unknown": {
const message = `Rejected unknown app-server request: ${request.method}`;
context.actions.rejectServerRequest(request.id, -32601, message);
return;
}
}
}
@ -181,10 +156,10 @@ function handleAppServerLog(context: ChatInboundHandlerContext, message: string)
}
function resolveApproval(context: ChatInboundHandlerContext, requestId: PendingRequestId, action: ApprovalAction): void {
const approval = pendingApproval(context, requestId);
const approval = state(context).requests.approvals.find((item) => item.requestId === requestId) ?? null;
if (!approval) return;
if (!context.actions.respondToServerRequest(approval.requestId, serverRequestApprovalResponse(approval, action))) {
addSystemMessage(context, cannotSendApprovalResponseMessage());
addSystemMessage(context, "Could not send approval response because Codex app-server is not connected.");
return;
}
dispatch(context, { type: "request/resolved", requestId: approval.requestId, resultItem: createApprovalResultItem(approval, action) });
@ -194,7 +169,7 @@ function resolveUserInput(context: ChatInboundHandlerContext, requestId: Pending
const input = pendingUserInput(context, requestId);
if (!input) return;
if (!context.actions.respondToServerRequest(input.requestId, serverRequestUserInputResponse(input.params.questions, answers))) {
addSystemMessage(context, cannotSendUserInputMessage());
addSystemMessage(context, "Could not send user input because Codex app-server is not connected.");
return;
}
dispatch(context, {
@ -207,8 +182,8 @@ function resolveUserInput(context: ChatInboundHandlerContext, requestId: Pending
function cancelUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId): void {
const input = pendingUserInput(context, requestId);
if (!input) return;
if (!context.actions.rejectServerRequest(input.requestId, -32000, userCancelledInputRequestMessage())) {
addSystemMessage(context, cannotCancelUserInputMessage());
if (!context.actions.rejectServerRequest(input.requestId, -32000, "User cancelled input request.")) {
addSystemMessage(context, "Could not cancel user input because Codex app-server is not connected.");
return;
}
dispatch(context, {
@ -219,11 +194,11 @@ function cancelUserInput(context: ChatInboundHandlerContext, requestId: PendingR
}
function resolveMcpElicitation(context: ChatInboundHandlerContext, requestId: PendingRequestId, action: McpElicitationAction): void {
const elicitation = pendingMcpElicitation(context, requestId);
const elicitation = state(context).requests.pendingMcpElicitations.find((item) => item.requestId === requestId) ?? null;
if (!elicitation) return;
const content = action === "accept" ? contentForPendingMcpElicitation(elicitation, state(context).requests.mcpElicitationDrafts) : null;
if (!context.actions.respondToServerRequest(elicitation.requestId, serverRequestMcpElicitationResponse(action, content))) {
addSystemMessage(context, cannotSendMcpElicitationMessage());
addSystemMessage(context, "Could not send MCP request response because Codex app-server is not connected.");
return;
}
dispatch(context, {
@ -233,27 +208,10 @@ function resolveMcpElicitation(context: ChatInboundHandlerContext, requestId: Pe
});
}
function respondToCurrentTimeRequest(
context: ChatInboundHandlerContext,
request: Extract<ServerRequest, { method: "currentTime/read" }>,
): void {
if (!context.actions.respondToServerRequest(request.id, serverRequestCurrentTimeResponse(Date.now()))) {
addSystemMessage(context, cannotSendCurrentTimeMessage());
}
}
function pendingApproval(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingApproval | null {
return state(context).requests.approvals.find((approval) => approval.requestId === requestId) ?? null;
}
function pendingUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingUserInput | null {
return state(context).requests.pendingUserInputs.find((input) => input.requestId === requestId) ?? null;
}
function pendingMcpElicitation(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingMcpElicitation | null {
return state(context).requests.pendingMcpElicitations.find((elicitation) => elicitation.requestId === requestId) ?? null;
}
function addSystemMessage(context: ChatInboundHandlerContext, text: string): void {
dispatch(context, { type: "message-stream/system-item-added", item: createSystemItem(localItemId(context, "system"), text) });
}
@ -269,40 +227,10 @@ function addDedupedSystemMessage(context: ChatInboundHandlerContext, text: strin
dispatch(context, { type: "message-stream/deduped-log-added", text, item: createSystemItem(localItemId(context, "system"), text) });
}
function queueApprovalRequest(context: ChatInboundHandlerContext, approval: PendingApproval): void {
dispatch(context, { type: "request/approval-queued", approval });
}
function queueUserInputRequest(context: ChatInboundHandlerContext, userInput: PendingUserInput): void {
dispatch(context, { type: "request/user-input-queued", input: userInput });
}
function queueMcpElicitationRequest(context: ChatInboundHandlerContext, elicitation: PendingMcpElicitation): void {
dispatch(context, { type: "request/mcp-elicitation-queued", elicitation });
}
function activeRouteScope(context: ChatInboundHandlerContext): { activeThreadId: string | null; activeTurnId: string | null } {
const current = state(context);
return {
activeThreadId: current.activeThread.id,
activeTurnId: activeTurnId(current),
};
}
function rejectUnsupportedServerRequest(context: ChatInboundHandlerContext, request: ServerRequest): void {
const message = `Rejected unsupported app-server request: ${request.method}`;
rejectServerRequest(context, request, message);
}
function rejectUnknownServerRequest(context: ChatInboundHandlerContext, request: ServerRequest): void {
const message = `Rejected unknown app-server request: ${request.method}`;
context.actions.rejectServerRequest(request.id, -32601, message);
}
function rejectServerRequest(context: ChatInboundHandlerContext, request: ServerRequest, message: string): void {
addSystemMessage(context, message);
if (!context.actions.rejectServerRequest(request.id, -32601, message)) {
addSystemMessage(context, cannotRejectServerRequestMessage());
addSystemMessage(context, "Could not reject app-server request because Codex app-server is not connected.");
}
}

View file

@ -437,7 +437,10 @@ function autoApprovalReviewPlan(
const reviewItem = createAutoReviewResultItem(notification.params);
return actionPlan({
type: "message-stream/items-replaced",
items: upsertMessageStreamItemById(removeUnstructuredAutoReviewWarnings(messageStreamItems(state.messageStream)), reviewItem),
items: upsertMessageStreamItemById(
messageStreamItems(state.messageStream).filter((item) => !isUnstructuredAutoReviewWarning(item)),
reviewItem,
),
});
}
@ -526,10 +529,6 @@ function messageStreamItemsWithPendingPromptSubmitHooks(state: ChatState, turnId
return attachHookRunsToTurn(items, turnId, pending.promptSubmitHookItemIds, pending.anchorItemId);
}
function removeUnstructuredAutoReviewWarnings(items: readonly MessageStreamItem[]): MessageStreamItem[] {
return items.filter((item) => !isUnstructuredAutoReviewWarning(item));
}
function hasStructuredAutoReviewResult(items: readonly MessageStreamItem[], activeTurnId: string | null): boolean {
return items.some(
(item) =>

View file

@ -3,7 +3,7 @@ import {
executionStateFromStatus,
RUNNING_EXECUTION_STATE,
} from "../../../domain/message-stream/execution-state";
import type { ExecutionState, HookMessageStreamItem } from "../../../domain/message-stream/items";
import type { HookMessageStreamItem } from "../../../domain/message-stream/items";
interface MessageStreamHookRun {
id: string;
@ -24,8 +24,9 @@ const HOOK_RUN_STATES: ExecutionStateByStatus = {
export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: string | null, status: string): HookMessageStreamItem | null {
if (run.id.length === 0) return null;
const eventName = hookEventName(run.eventName);
const displayId = hookRunDisplayId(run);
const trimmedEventName = run.eventName?.trim();
const eventName = trimmedEventName && trimmedEventName.length > 0 ? trimmedEventName : "Hook";
const displayId = `hook-${run.id}-${run.startedAt.toString()}`;
return {
id: displayId,
kind: "hook",
@ -37,7 +38,7 @@ export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: stri
sourceItemId: displayId,
provenance: { source: "appServer", channel: "notification", event: "hookRun", sourceItemId: displayId },
status,
executionState: hookRunExecutionState(status),
executionState: executionStateFromStatus(status, HOOK_RUN_STATES),
hookRun: {
eventName,
...definedProp("statusMessage", run.statusMessage ?? undefined),
@ -48,19 +49,6 @@ export function hookRunMessageStreamItem(run: MessageStreamHookRun, turnId: stri
};
}
function hookRunExecutionState(status: string): ExecutionState {
return executionStateFromStatus(status, HOOK_RUN_STATES);
}
function hookRunDisplayId(run: MessageStreamHookRun): string {
return `hook-${run.id}-${run.startedAt.toString()}`;
}
function hookEventName(eventName: string | null | undefined): string {
const trimmed = eventName?.trim();
return trimmed && trimmed.length > 0 ? trimmed : "Hook";
}
function definedProp<Key extends string, Value>(key: Key, value: Value | null | undefined): Record<Key, Value> | Record<string, never> {
return value === null || value === undefined ? {} : ({ [key]: value } as Record<Key, Value>);
}

View file

@ -3,7 +3,7 @@ import {
executionStateFromStatus,
RUNNING_EXECUTION_STATE,
} from "../../../domain/message-stream/execution-state";
import type { ExecutionState, MessageStreamItem } from "../../../domain/message-stream/items";
import type { MessageStreamItem } from "../../../domain/message-stream/items";
const TASK_STATES = {
pending: RUNNING_EXECUTION_STATE,
@ -18,10 +18,6 @@ interface TaskPlanStep {
status: TaskStepStatus;
}
function taskProgressExecutionState(status: string): ExecutionState {
return executionStateFromStatus(status, TASK_STATES);
}
export function taskProgressMessageStreamItem(
turnId: string,
explanation: string | null,
@ -39,6 +35,6 @@ export function taskProgressMessageStreamItem(
explanation: trimmedExplanation !== undefined && trimmedExplanation.length > 0 ? trimmedExplanation : null,
steps: plan.map((step) => ({ step: step.step, status: step.status })),
status,
executionState: taskProgressExecutionState(status),
executionState: executionStateFromStatus(status, TASK_STATES),
};
}

View file

@ -48,13 +48,11 @@ export function composerBoundaryScrollDirection(
if (keyAction.kind === "scroll-to" || keyAction.amount === "page") return keyAction;
if (composer.cursorStart !== composer.cursorEnd) return null;
return keyAction.direction === -1
? composerCursorOnFirstLine(composer) && composerCursorAtVisualBoundary(keyAction.direction, composer, options)
? keyAction
: null
: composerCursorOnLastLine(composer) && composerCursorAtVisualBoundary(keyAction.direction, composer, options)
? keyAction
: null;
const cursorAtTextBoundary =
keyAction.direction === -1
? !composer.value.slice(0, composer.cursorStart).includes("\n")
: !composer.value.slice(composer.cursorEnd).includes("\n");
return cursorAtTextBoundary && composerCursorAtVisualBoundary(keyAction.direction, composer, options) ? keyAction : null;
}
function composerBoundaryScrollKeyAction(event: ComposerBoundaryScrollKeyEvent): ComposerBoundaryScrollAction | null {
@ -82,14 +80,6 @@ function composerBoundaryScrollByAction(
return repeat ? { kind: "scroll-by", direction, amount, repeated: true } : { kind: "scroll-by", direction, amount };
}
function composerCursorOnFirstLine(composer: ComposerBoundaryScrollTextState): boolean {
return !composer.value.slice(0, composer.cursorStart).includes("\n");
}
function composerCursorOnLastLine(composer: ComposerBoundaryScrollTextState): boolean {
return !composer.value.slice(composer.cursorEnd).includes("\n");
}
function composerCursorAtVisualBoundary(
direction: ComposerBoundaryScrollDirection,
composer: ComposerBoundaryScrollTextState,

View file

@ -165,11 +165,7 @@ async function initializeConnection(host: ChatConnectionControllerHost, connecti
function connectionErrorMessage(error: unknown, configuredCommand: string): string {
const message = error instanceof Error ? error.message : String(error);
if (!isMissingCommandError(error)) return message;
return missingCommandConnectionErrorMessage(message, configuredCommand);
}
function missingCommandConnectionErrorMessage(errorMessage: string, configuredCommand: string): string {
return `Could not start Codex app-server because the configured command was not found: ${configuredCommand}. Check the Codex command path in settings. (${errorMessage})`;
return `Could not start Codex app-server because the configured command was not found: ${configuredCommand}. Check the Codex command path in settings. (${message})`;
}
function isMissingCommandError(error: unknown): boolean {

View file

@ -64,12 +64,6 @@ function diagnosticProbeRow(probe: DiagnosticProbeResult): DiagnosticRow {
return {
label: probe.method,
value: `${probe.status}${detail}`,
level: diagnosticProbeLevel(probe.status),
level: probe.status === "failed" ? "error" : probe.status === "unknown" ? "warning" : "normal",
};
}
function diagnosticProbeLevel(status: DiagnosticProbeResult["status"]): NonNullable<DiagnosticRow["level"]> {
if (status === "failed") return "error";
if (status === "unknown") return "warning";
return "normal";
}

View file

@ -1,6 +1,6 @@
import type { SkillMetadata } from "../../../../domain/catalog/metadata";
import type { Diagnostics, McpServerDiagnostic, McpServerStatusSummary } from "../../../../domain/server/diagnostics";
import type { ToolInventoryApp, ToolInventoryPlugin, ToolInventorySnapshot } from "../../../../domain/server/tool-inventory";
import type { ToolInventoryPlugin, ToolInventorySnapshot } from "../../../../domain/server/tool-inventory";
import type { DiagnosticRow, DiagnosticSection } from "./diagnostic-sections";
const PERSONAL_SKILLS_LABEL = "Personal";
@ -50,7 +50,7 @@ function pluginRows(inventory: ToolInventorySnapshot): DiagnosticRow[] {
if (inventory.pluginsError) return [{ label: "Plugins", value: inventory.pluginsError, level: "error" }];
if (!inventory.plugins) return [{ label: "Plugins", value: "not loaded", level: "warning" }];
const rows = inventory.plugins.filter(isUsablePlugin).map(pluginRow);
const rows = inventory.plugins.filter((plugin) => plugin.enabled && plugin.installed).map(pluginRow);
return rows.length > 0 ? rows : [{ label: "Plugins", value: "(none)" }];
}
@ -70,7 +70,10 @@ function toolProviderRows(inventory: ToolInventorySnapshot): DiagnosticRow[] {
function codexAppsProviderRow(inventory: ToolInventorySnapshot): DiagnosticRow {
if (inventory.appsError) return { label: CODEX_APPS_PROVIDER_LABEL, value: inventory.appsError, level: "error" };
if (!inventory.apps) return { label: CODEX_APPS_PROVIDER_LABEL, value: "not loaded", level: "warning" };
return { label: CODEX_APPS_PROVIDER_LABEL, value: listSummary(inventory.apps.filter(isUsableApp).map((app) => app.name)) };
return {
label: CODEX_APPS_PROVIDER_LABEL,
value: listSummary(inventory.apps.filter((app) => app.enabled && app.accessible).map((app) => app.name)),
};
}
function mcpToolProviderRows(inventory: ToolInventorySnapshot): DiagnosticRow[] {
@ -153,14 +156,6 @@ function skillRows(inventory: ToolInventorySnapshot): DiagnosticRow[] {
.map(([provenance, skills]) => ({ label: provenance, value: listSummary([...skills]) }));
}
function isUsableApp(app: ToolInventoryApp): boolean {
return app.enabled && app.accessible;
}
function isUsablePlugin(plugin: ToolInventoryPlugin): boolean {
return plugin.enabled && plugin.installed;
}
function pluginBundleSummary(plugin: ToolInventoryPlugin): string {
if (plugin.detailsError) return "details unavailable";
if (!plugin.details) return "details not loaded";

View file

@ -2,7 +2,7 @@ import type { AppServerClient } from "../../../../app-server/connection/client";
import { latestImplementablePlanTargetFromItems, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
import type { ChatRuntimeState } from "../../domain/runtime/state";
import { type ChatMessageStreamState, messageStreamItems } from "../state/message-stream";
import type { ChatActiveThreadState, ChatState } from "../state/root-reducer";
import type { ChatActiveThreadState } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
import { type ChatTurnState, chatTurnBusy } from "./turn-state";
@ -15,10 +15,6 @@ export interface PlanImplementationHost {
requestDefaultCollaborationModeForNextTurn(): void;
}
function canImplementPlanItemId(state: ChatState, itemId: string): boolean {
return itemId === implementPlanTargetFromState(state)?.itemId;
}
export function implementPlanTargetFromState(state: {
activeThread: Pick<ChatActiveThreadState, "id">;
turn: ChatTurnState;
@ -32,7 +28,7 @@ export function implementPlanTargetFromState(state: {
}
export async function implementPlan(host: PlanImplementationHost, itemId: string): Promise<void> {
if (!canImplementPlanItemId(host.stateStore.getState(), itemId)) return;
if (itemId !== implementPlanTargetFromState(host.stateStore.getState())?.itemId) return;
if (!(await host.connectedClient()) || !host.stateStore.getState().activeThread.id) return;
host.requestDefaultCollaborationModeForNextTurn();

View file

@ -77,22 +77,6 @@ export interface ThreadReferenceInput {
referencedThread: ReferencedThreadMetadata;
}
function currentThreadReferenceMessage(): string {
return "Use the current thread directly instead of referencing it.";
}
function noActiveThreadToForkMessage(): string {
return "No active thread to fork.";
}
function noActiveThreadToRollbackMessage(): string {
return "No active thread to roll back.";
}
function noActiveThreadToCompactMessage(): string {
return "No active thread to compact.";
}
export async function executeSlashCommand(
command: SlashCommandName,
args: string,
@ -132,7 +116,7 @@ export async function executeSlashCommand(
return;
}
if (thread.thread.id === context.activeThreadId) {
context.addSystemMessage(currentThreadReferenceMessage());
context.addSystemMessage("Use the current thread directly instead of referencing it.");
return;
}
const reference = await context.referThread(thread.thread, parsed.message);
@ -141,21 +125,21 @@ export async function executeSlashCommand(
}
case "fork":
if (!context.activeThreadId) {
context.addSystemMessage(noActiveThreadToForkMessage());
context.addSystemMessage("No active thread to fork.");
return;
}
await context.threadActions.forkThread(context.activeThreadId);
return;
case "rollback":
if (!context.activeThreadId) {
context.addSystemMessage(noActiveThreadToRollbackMessage());
context.addSystemMessage("No active thread to roll back.");
return;
}
await context.threadActions.rollbackThread(context.activeThreadId);
return;
case "compact":
if (!context.activeThreadId) {
context.addSystemMessage(noActiveThreadToCompactMessage());
context.addSystemMessage("No active thread to compact.");
return;
}
await context.threadActions.compactThread(context.activeThreadId);

View file

@ -25,14 +25,6 @@ export interface SlashCommandExecutorHost extends SlashCommandExecutionPorts {
setStatus: (status: string) => void;
}
function referencedThreadStatus(thread: Thread, includedTurns: number): string {
return `Referencing ${shortThreadId(thread.id)} (${String(includedTurns)}/${String(REFERENCED_THREAD_TURN_LIMIT)} turns).`;
}
function referencedThreadUnreadableMessage(): string {
return "Referenced thread has no readable conversation turns.";
}
export async function executeSlashCommandWithState(
host: SlashCommandExecutorHost,
command: SlashCommandName,
@ -71,12 +63,12 @@ async function referencedThreadInput(
try {
const turns = await readReferencedThreadConversationSummaries(client, thread.id, REFERENCED_THREAD_TURN_LIMIT);
if (turns.length === 0) {
host.addSystemMessage(referencedThreadUnreadableMessage());
host.addSystemMessage("Referenced thread has no readable conversation turns.");
return null;
}
const reference = referencedThreadPromptBundle(thread, turns, message);
const messageInput = host.codexInput(message);
host.setStatus(referencedThreadStatus(thread, turns.length));
host.setStatus(`Referencing ${shortThreadId(thread.id)} (${String(turns.length)}/${String(REFERENCED_THREAD_TURN_LIMIT)} turns).`);
return {
input: codexTextInputWithAttachments(reference.prompt, messageInput),
referencedThread: reference.referencedThread,

View file

@ -39,10 +39,6 @@ type TurnSubmissionPlan =
| { kind: "start-thread-then-turn" }
| { kind: "start-turn"; threadId: string };
function currentTurnNotSteerableMessage(): string {
return "Current turn is not steerable yet.";
}
export interface TurnSubmissionActions {
sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadMetadata): Promise<void>;
}
@ -148,7 +144,7 @@ function planTurnSubmission(state: TurnSubmissionSnapshot): TurnSubmissionPlan {
if (state.busy) {
return state.activeThreadId && state.activeTurnId
? { kind: "steer", threadId: state.activeThreadId, turnId: state.activeTurnId }
: { kind: "blocked", message: currentTurnNotSteerableMessage() };
: { kind: "blocked", message: "Current turn is not steerable yet." };
}
return state.activeThreadId ? { kind: "start-turn", threadId: state.activeThreadId } : { kind: "start-thread-then-turn" };
}

View file

@ -2,8 +2,6 @@ import {
type ApprovalAction,
answersForPendingUserInput,
type McpElicitationAction,
type PendingApproval,
type PendingMcpElicitation,
type PendingRequestId,
type PendingUserInput,
} from "../../../../domain/pending-requests/model";
@ -41,7 +39,7 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
let lastFocusSignature = "";
const resolveApproval = (requestId: PendingRequestId, approvalAction: ApprovalAction): void => {
const approval = pendingApproval(host, requestId);
const approval = host.stateStore.getState().requests.approvals.find((item) => item.requestId === requestId) ?? null;
if (!approval) return;
host.responder.resolveApproval(requestId, approvalAction);
commitRequestAction(host);
@ -65,7 +63,7 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
};
const resolveMcpElicitation = (requestId: PendingRequestId, action: McpElicitationAction): void => {
const elicitation = pendingMcpElicitation(host, requestId);
const elicitation = host.stateStore.getState().requests.pendingMcpElicitations.find((item) => item.requestId === requestId) ?? null;
if (!elicitation) return;
host.responder.resolveMcpElicitation(requestId, action);
commitRequestAction(host);
@ -132,18 +130,10 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
};
}
function pendingApproval(host: PendingRequestActionsHost, requestId: PendingRequestId): PendingApproval | null {
return host.stateStore.getState().requests.approvals.find((approval) => approval.requestId === requestId) ?? null;
}
function pendingUserInput(host: PendingRequestActionsHost, requestId: PendingRequestId): PendingUserInput | null {
return host.stateStore.getState().requests.pendingUserInputs.find((input) => input.requestId === requestId) ?? null;
}
function pendingMcpElicitation(host: PendingRequestActionsHost, requestId: PendingRequestId): PendingMcpElicitation | null {
return host.stateStore.getState().requests.pendingMcpElicitations.find((elicitation) => elicitation.requestId === requestId) ?? null;
}
function commitRequestAction(host: PendingRequestActionsHost): void {
host.refreshLiveState();
host.focusComposer();

View file

@ -1,7 +1,6 @@
import type { AppServerClient } from "../../../../app-server/connection/client";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import { type RuntimeConfigSnapshot, runtimeConfigOrDefault } from "../../../../domain/runtime/config";
import type { ApprovalsReviewer } from "../../../../domain/runtime/policy";
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
import {
pendingRuntimeSettingsPatch as buildPendingRuntimeSettingsPatch,
@ -148,7 +147,7 @@ async function setFastMode(host: RuntimeSettingsActionsHost, mode: FastModeState
dispatch(host, { type: "runtime/fast-mode-requested", fastMode });
return applyPendingThreadSettings(host);
},
fastModeToggleMessage(mode),
mode === "enabled" ? "Fast mode on for subsequent turns." : "Fast mode off for subsequent turns.",
);
}
@ -162,7 +161,9 @@ async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborat
dispatch(host, { type: "runtime/requested-collaboration-mode-set", collaborationMode });
const result = await commitPendingThreadSettings(host);
if (result.ok) closeRuntimePanel(host);
if (result.ok && result.collaborationModeApplied) host.addSystemMessage(collaborationModeToggleMessage(collaborationMode));
if (result.ok && result.collaborationModeApplied) {
host.addSystemMessage(collaborationMode === "plan" ? "Plan mode on for subsequent turns." : "Plan mode off for subsequent turns.");
}
return result.ok;
}
@ -172,7 +173,7 @@ function requestDefaultCollaborationModeForNextTurn(host: RuntimeSettingsActions
async function toggleAutoReview(host: RuntimeSettingsActionsHost): Promise<void> {
const { snapshot, config } = runtimeProjection(host);
const nextState = nextAutoReviewState(resolveRuntimeControls(snapshot, config).autoReview.active);
const nextState = resolveRuntimeControls(snapshot, config).autoReview.active ? "disabled" : "enabled";
await setAutoReview(host, nextState);
}
@ -180,30 +181,13 @@ async function setAutoReview(host: RuntimeSettingsActionsHost, mode: AutoReviewS
await runRuntimeUiCommand(
host,
async () => {
dispatch(host, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: autoReviewReviewerForState(mode) });
dispatch(host, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: mode === "enabled" ? "auto_review" : "user" });
return applyPendingThreadSettings(host);
},
autoReviewToggleMessage(mode),
mode === "enabled" ? "Auto-review on for subsequent turns." : "Auto-review off for subsequent turns.",
);
}
function fastModeToggleMessage(state: FastModeState): string {
return state === "enabled" ? "Fast mode on for subsequent turns." : "Fast mode off for subsequent turns.";
}
function collaborationModeToggleMessage(mode: CollaborationModeSelection): string {
return mode === "plan" ? "Plan mode on for subsequent turns." : "Plan mode off for subsequent turns.";
}
function autoReviewToggleMessage(state: AutoReviewState): string {
return state === "enabled" ? "Auto-review on for subsequent turns." : "Auto-review off for subsequent turns.";
}
function collaborationModeWarningMessage(warning: NonNullable<PendingRuntimeSettingsPatch["collaborationModeWarning"]>): string {
void warning;
return "No effective model is available. Sending without a mode override.";
}
async function runRuntimeUiCommand(
host: RuntimeSettingsActionsHost,
command: () => Promise<boolean>,
@ -218,14 +202,6 @@ function closeRuntimePanel(host: RuntimeSettingsActionsHost): void {
dispatch(host, { type: "ui/panel-set", panel: null });
}
function nextAutoReviewState(active: boolean): AutoReviewState {
return active ? "disabled" : "enabled";
}
function autoReviewReviewerForState(state: AutoReviewState): ApprovalsReviewer {
return state === "enabled" ? "auto_review" : "user";
}
function pendingRuntimeSettingsPatch(host: RuntimeSettingsActionsHost): PendingRuntimeSettingsPatch {
const { snapshot, config } = runtimeProjection(host);
return buildPendingRuntimeSettingsPatch(snapshot, config);
@ -235,7 +211,10 @@ function reportCollaborationModeWarning(
host: RuntimeSettingsActionsHost,
warning: NonNullable<PendingRuntimeSettingsPatch["collaborationModeWarning"]>,
): void {
host.addSystemMessage(`${host.collaborationModeLabel()} mode is selected, but ${collaborationModeWarningMessage(warning)}`);
void warning;
host.addSystemMessage(
`${host.collaborationModeLabel()} mode is selected, but No effective model is available. Sending without a mode override.`,
);
}
function currentPendingRuntimeSettingsPatch(host: RuntimeSettingsActionsHost): RuntimeSettingsPatch {

View file

@ -121,15 +121,11 @@ export function messageStreamIsEmpty(state: Pick<ChatMessageStreamState, "stable
return state.stableItems.length === 0 && (!state.activeSegment || state.activeSegment.items.length === 0);
}
function messageStreamTurnIds(state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">): string[] {
return orderedTurnIds(messageStreamItems(state));
}
export function messageStreamTurnsAfterTurnId(
state: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">,
turnId: string,
): number | null {
const turnIds = messageStreamTurnIds(state);
const turnIds = orderedTurnIds(messageStreamItems(state));
const index = turnIds.indexOf(turnId);
return index === -1 ? null : turnIds.length - index - 1;
}
@ -191,7 +187,7 @@ export function reduceMessageStreamSlice(state: ChatMessageStreamState, action:
switch (action.type) {
case "message-stream/item-added":
case "message-stream/system-item-added":
return appendMessageStreamItem(state, action.item);
return patchObject(state, appendMessageStreamItemPatch(state, action.item));
case "message-stream/deduped-log-added":
if (state.reportedLogs.has(action.text)) return state;
return patchObject(state, {
@ -226,10 +222,6 @@ export function reduceMessageStreamSlice(state: ChatMessageStreamState, action:
}
}
function appendMessageStreamItem(state: ChatMessageStreamState, item: MessageStreamItem): ChatMessageStreamState {
return patchObject(state, appendMessageStreamItemPatch(state, item));
}
function appendMessageStreamItemPatch(state: ChatMessageStreamState, item: MessageStreamItem): Partial<ChatMessageStreamState> {
if (shouldUseActiveSegment(state.activeSegment, item)) {
return { activeSegment: appendActiveSegmentItem(state.activeSegment, item) };
@ -459,7 +451,7 @@ function appendActiveSegmentItem(segment: ChatMessageStreamActiveSegment, item:
function upsertActiveSegmentItem(segment: ChatMessageStreamActiveSegment, item: MessageStreamItem): ChatMessageStreamActiveSegment {
const index = segment.indexById.get(item.id);
if (index === undefined) return appendActiveSegmentItem(segment, item);
return replaceActiveSegmentItem(segment, index, (previous) => mergeMessageStreamItem(previous, item));
return replaceActiveSegmentItem(segment, index, (previous) => upsertMessageStreamItemById([previous], item)[0] ?? item);
}
function replaceActiveSegmentItem(
@ -476,10 +468,6 @@ function replaceActiveSegmentItem(
return activeSegmentFromItems(segment.turnId, items);
}
function mergeMessageStreamItem(previous: MessageStreamItem, next: MessageStreamItem): MessageStreamItem {
return upsertMessageStreamItemById([previous], next)[0] ?? next;
}
function activeSegmentFromItems(turnId: string | null, items: readonly MessageStreamItem[]): ChatMessageStreamActiveSegment {
const indexById = new Map<string, number>();
const indexBySourceItemId = new Map<string, number>();

View file

@ -84,10 +84,6 @@ export type ChatConnectionPhase =
| { kind: "failed"; message: string }
| { kind: "disconnected"; message: string };
function turnCompletedStatus(status: string): string {
return `Turn ${status}.`;
}
interface ChatConnectionState {
readonly phase: ChatConnectionPhase;
readonly statusText: string;
@ -244,7 +240,7 @@ export function createChatState(): ChatState {
connection: initialConnectionState(),
threadList: initialThreadListState(),
activeThread: initialActiveThreadState(),
runtime: initialRuntimeState(),
runtime: initialChatRuntimeState(),
turn: initialTurnState(),
messageStream: initialMessageStreamState(),
requests: initialRequestState(),
@ -394,7 +390,7 @@ function reduceTurnCompletedTransition(state: ChatState, action: TurnCompletedAc
return patchChatState(state, {
turn: { lifecycle },
messageStream: messageStreamWithItems(state.messageStream, action.items),
connection: { ...state.connection, statusText: turnCompletedStatus(action.status) },
connection: { ...state.connection, statusText: `Turn ${action.status}.` },
});
}
@ -630,10 +626,6 @@ function initialActiveThreadState(): ChatActiveThreadState {
};
}
function initialRuntimeState(): ChatRuntimeState {
return initialChatRuntimeState();
}
function initialTurnState(): ChatTurnState {
return initialChatTurnState();
}

View file

@ -2,7 +2,6 @@ import type { Thread } from "../../../../domain/threads/model";
import type { ThreadTitleContext } from "../../../../domain/threads/title-generation-model";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import type { ThreadTitleService } from "../../../threads/thread-title-service";
import type { ChatState } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
export interface AutoTitleCoordinatorHost {
@ -22,7 +21,8 @@ export function createAutoTitleCoordinator(host: AutoTitleCoordinatorHost): Auto
const attemptedThreadIds = new Set<string>();
const inFlightThreadIds = new Set<string>();
const thread = (threadId: string): Thread | undefined => state(host).threadList.listedThreads.find((item) => item.id === threadId);
const thread = (threadId: string): Thread | undefined =>
host.stateStore.getState().threadList.listedThreads.find((item) => item.id === threadId);
const threadHasTitle = (threadId: string): boolean => Boolean(thread(threadId)?.name?.trim());
const threadCanReceiveGeneratedTitle = (threadId: string): boolean => {
const candidate = thread(threadId);
@ -64,7 +64,3 @@ export function createAutoTitleCoordinator(host: AutoTitleCoordinatorHost): Auto
},
};
}
function state(host: AutoTitleCoordinatorHost): ChatState {
return host.stateStore.getState();
}

View file

@ -44,9 +44,7 @@ type GoalObjectiveSavePlan =
type NormalizedGoalObjective = string & { readonly __brand: "NormalizedGoalObjective" };
function emptyGoalObjectiveMessage(): string {
return "Goal objective cannot be empty.";
}
const EMPTY_GOAL_OBJECTIVE_MESSAGE = "Goal objective cannot be empty.";
export function createThreadGoalSyncActions(host: ThreadGoalSyncHost): ThreadGoalSyncActions {
return {
@ -93,7 +91,7 @@ async function syncThreadGoal(host: ThreadGoalSyncHost, threadId: string): Promi
async function setObjective(host: GoalActionsHost, threadId: string, objective: string, tokenBudget: number | null): Promise<boolean> {
const normalized = normalizedGoalObjective(objective);
if (!normalized) {
host.addSystemMessage(emptyGoalObjectiveMessage());
host.addSystemMessage(EMPTY_GOAL_OBJECTIVE_MESSAGE);
return false;
}
return setNormalizedObjective(host, threadId, normalized, tokenBudget);
@ -186,7 +184,7 @@ function startEditing(host: GoalActionsHost, threadId: string | null, objective:
function planGoalObjectiveSave(activeThreadId: string | null, objective: string, tokenBudget: number | null): GoalObjectiveSavePlan {
const normalized = normalizedGoalObjective(objective);
if (!normalized) return { kind: "reject", message: emptyGoalObjectiveMessage() };
if (!normalized) return { kind: "reject", message: EMPTY_GOAL_OBJECTIVE_MESSAGE };
return activeThreadId
? { kind: "save-existing", threadId: activeThreadId, objective: normalized, tokenBudget }
: { kind: "start-thread-and-save", objective: normalized, tokenBudget };

View file

@ -1,4 +1,3 @@
import type { Thread } from "../../../../domain/threads/model";
import { threadRenameDraftTitle } from "../../../../domain/threads/title";
import type { ThreadTitleContext } from "../../../../domain/threads/title-generation-model";
import type { ThreadOperations } from "../../../threads/thread-operations";
@ -50,7 +49,7 @@ export function createThreadRenameEditorActions(host: ThreadRenameEditorActionsH
},
start(threadId: string): void {
const thread = threadById(host, threadId);
const thread = host.stateStore.getState().threadList.listedThreads.find((item) => item.id === threadId);
if (!thread) return;
dispatch(host, { type: "ui/rename-started", threadId, draft: threadRenameDraftTitle(thread) });
},
@ -134,7 +133,3 @@ function finishAutoNameDraftGeneration(
): void {
dispatch(host, { type: "ui/rename-generation-finished", threadId, generatingState });
}
function threadById(host: ThreadRenameEditorActionsHost, threadId: string): Thread | undefined {
return host.stateStore.getState().threadList.listedThreads.find((item) => item.id === threadId);
}

View file

@ -36,17 +36,9 @@ export function createResumeActions(host: ResumeActionsHost): ResumeActions {
};
}
function finishBeforeSwitchingThreadsMessage(): string {
return "Finish or interrupt the current turn before switching threads.";
}
function resumedThreadMessage(threadId: string): string {
return `Resumed thread ${threadId}`;
}
async function resumeThread(host: ResumeActionsHost, threadId: string): Promise<void> {
if (!canSwitchToThread(host.stateStore.getState(), threadId)) {
host.addSystemMessage(finishBeforeSwitchingThreadsMessage());
host.addSystemMessage("Finish or interrupt the current turn before switching threads.");
return;
}
const resume = host.resumeWork.begin(threadId);
@ -70,7 +62,7 @@ async function resumeThread(host: ResumeActionsHost, threadId: string): Promise<
if (isStaleResume(host, resume)) return;
const renderFallbackMessage = messageStreamIsEmpty(host.stateStore.getState().messageStream);
if (renderFallbackMessage) {
host.addSystemMessage(resumedThreadMessage(response.activation.thread.id));
host.addSystemMessage(`Resumed thread ${response.activation.thread.id}`);
}
host.refreshLiveState();
} catch (error) {

View file

@ -68,50 +68,10 @@ export function createThreadManagementActions(host: ThreadManagementActionsHost)
};
}
function finishBeforeArchivingThreadsMessage(): string {
return "Finish or interrupt the current turn before archiving threads.";
}
function finishBeforeForkingThreadsMessage(): string {
return "Finish or interrupt the current turn before forking threads.";
}
function selectedTurnNotFoundForForkMessage(): string {
return "Could not find the selected turn to fork.";
}
function forkNameCopyFailedMessage(threadId: string, message: string): string {
return `Forked thread ${threadId}, but could not copy the source thread name: ${message}`;
}
function archivedSourceOpenForkFailedMessage(sourceThreadId: string, forkedThreadId: string, message: string): string {
return `Archived thread ${sourceThreadId}, but could not open forked thread ${forkedThreadId}: ${message}`;
}
function openForkInNewPanelFailedMessage(forkedThreadId: string, message: string): string {
return `Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`;
}
function interruptBeforeRollbackMessage(): string {
return "Interrupt the current turn before rolling back.";
}
function noCompletedTurnToRollbackMessage(): string {
return "No completed turn to roll back.";
}
function noActiveThreadToCompactMessage(): string {
return "No active thread to compact.";
}
function rollbackCompletedMessage(): string {
return "Rolled back the latest turn. Local file changes were not reverted.";
}
async function compactActiveThread(host: ThreadManagementActionsHost): Promise<void> {
const threadId = threadManagementState(host).activeThread.id;
if (!threadId) {
host.addSystemMessage(noActiveThreadToCompactMessage());
host.addSystemMessage("No active thread to compact.");
return;
}
await compactThread(host, threadId);
@ -136,7 +96,7 @@ async function archiveThread(host: ThreadManagementActionsHost, threadId: string
async function archiveThreadFromPanel(host: ThreadManagementActionsHost, threadId: string, saveMarkdown?: boolean): Promise<boolean> {
if (chatTurnBusy(threadManagementState(host))) {
host.addSystemMessage(finishBeforeArchivingThreadsMessage());
host.addSystemMessage("Finish or interrupt the current turn before archiving threads.");
return false;
}
try {
@ -159,7 +119,7 @@ async function forkThreadFromTurn(
archiveSource: boolean,
): Promise<void> {
if (chatTurnBusy(threadManagementState(host))) {
host.addSystemMessage(finishBeforeForkingThreadsMessage());
host.addSystemMessage("Finish or interrupt the current turn before forking threads.");
return;
}
const scope = await captureThreadManagementOperationScope(host, threadId);
@ -167,7 +127,7 @@ async function forkThreadFromTurn(
const turnsToDrop = turnId ? messageStreamTurnsAfterTurnId(threadManagementState(host).messageStream, turnId) : 0;
if (turnsToDrop === null) {
host.addSystemMessage(selectedTurnNotFoundForForkMessage());
host.addSystemMessage("Could not find the selected turn to fork.");
return;
}
@ -188,7 +148,7 @@ async function forkThreadFromTurn(
if (!(await host.operations.renameThread(forkedThreadId, sourceName))) return;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(forkNameCopyFailedMessage(forkedThreadId, message));
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not copy the source thread name: ${message}`);
}
}
if (archiveSource) {
@ -197,7 +157,7 @@ async function forkThreadFromTurn(
await host.openThreadInCurrentPanel(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(archivedSourceOpenForkFailedMessage(threadId, forkedThreadId, message));
host.addSystemMessage(`Archived thread ${threadId}, but could not open forked thread ${forkedThreadId}: ${message}`);
}
return;
}
@ -205,7 +165,7 @@ async function forkThreadFromTurn(
await host.openThreadInNewView(forkedThreadId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
host.addSystemMessage(openForkInNewPanelFailedMessage(forkedThreadId, message));
host.addSystemMessage(`Forked thread ${forkedThreadId}, but could not open it in a new panel: ${message}`);
}
} catch (error) {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
@ -225,7 +185,7 @@ async function renameThread(host: ThreadManagementActionsHost, threadId: string,
async function rollbackThread(host: ThreadManagementActionsHost, threadId: string): Promise<void> {
if (chatTurnBusy(threadManagementState(host))) {
host.addSystemMessage(interruptBeforeRollbackMessage());
host.addSystemMessage("Interrupt the current turn before rolling back.");
return;
}
const scope = await captureThreadManagementOperationScope(host, threadId);
@ -233,7 +193,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
const candidate = messageStreamRollbackCandidate(threadManagementState(host).messageStream);
if (!candidate) {
host.addSystemMessage(noCompletedTurnToRollbackMessage());
host.addSystemMessage("No completed turn to roll back.");
return;
}
@ -257,7 +217,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
loadingHistory: false,
});
host.setComposerText(candidate.text);
host.addSystemMessage(rollbackCompletedMessage());
host.addSystemMessage("Rolled back the latest turn. Local file changes were not reverted.");
host.setStatus(STATUS_ROLLBACK_COMPLETE);
host.notifyActiveThreadIdentityChanged();
await host.refreshAfterThreadMutation();

View file

@ -19,14 +19,10 @@ export interface ThreadNavigationActions {
selectThreadFromToolbar(threadId: string): Promise<void>;
}
function finishBeforeSwitchingThreadsMessage(): string {
return "Finish or interrupt the current turn before switching threads.";
}
export function createThreadNavigationActions(host: ThreadNavigationActionsHost): ThreadNavigationActions {
const selectThread = async (threadId: string): Promise<void> => {
if (!canSwitchToThread(host.stateStore.getState(), threadId)) {
host.addSystemMessage(finishBeforeSwitchingThreadsMessage());
host.addSystemMessage("Finish or interrupt the current turn before switching threads.");
return;
}

View file

@ -65,7 +65,7 @@ export function resolveRuntimeControls(snapshot: RuntimeSnapshot, config: Runtim
pending: snapshot.pending.approvalsReviewer,
});
const autoReview = resolveAutoReview(approvalsReviewer);
const serviceTiers = modelServiceTiers(snapshot.availableModels, model.effective);
const serviceTiers = findModelMetadataByIdOrName(snapshot.availableModels, model.effective)?.serviceTiers ?? [];
const serviceTier = resolveServiceTier(snapshot, config);
const fastMode = resolveFastMode(snapshot.pending.fastMode, serviceTier, serviceTiers);
const collaborationMode = resolveCollaborationMode(snapshot, model.effective);
@ -84,7 +84,7 @@ export function resolveRuntimeControls(snapshot: RuntimeSnapshot, config: Runtim
function resolveAutoReview(approvalsReviewer: RuntimeLayeredValue<ApprovalsReviewer>): AutoReviewResolution {
return {
active: isAutoReviewReviewer(approvalsReviewer.effective),
active: approvalsReviewer.effective === "auto_review" || approvalsReviewer.effective === "guardian_subagent",
reviewer: approvalsReviewer.effective,
source: approvalsReviewer.source,
};
@ -159,7 +159,7 @@ function resolveFastMode(
active: isFastServiceTier(serviceTier.effective, serviceTiers),
source: serviceTier.source,
effectiveServiceTier: serviceTier.effective,
serviceTierRequestValue: fastServiceTierRequestValue(serviceTiers),
serviceTierRequestValue: serviceTiers.find((tier) => tier.name.trim().toLowerCase() === "fast")?.id ?? "fast",
};
}
@ -183,15 +183,3 @@ function isFastServiceTier(value: string | null | undefined, serviceTiers: Model
if (serviceTiers.length === 0) return value === "priority";
return serviceTiers.some((tier) => tier.id === value && tier.name.trim().toLowerCase() === "fast");
}
function isAutoReviewReviewer(value: ApprovalsReviewer | null): boolean {
return value === "auto_review" || value === "guardian_subagent";
}
function fastServiceTierRequestValue(serviceTiers: ModelMetadata["serviceTiers"]): string {
return serviceTiers.find((tier) => tier.name.trim().toLowerCase() === "fast")?.id ?? "fast";
}
function modelServiceTiers(models: readonly ModelMetadata[], model: string | null): ModelMetadata["serviceTiers"] {
return findModelMetadataByIdOrName(models, model)?.serviceTiers ?? [];
}

View file

@ -9,7 +9,7 @@ import type {
} from "../../domain/message-stream/items";
import { messageStreamReasoningIsActive } from "../../domain/message-stream/semantics/active-turn";
export type StatusChecklistItem = TaskProgressMessageStreamItem["steps"][number];
type StatusChecklistItem = TaskProgressMessageStreamItem["steps"][number];
export type MessageStreamStatusView =
| {

View file

@ -1,7 +1,7 @@
import type { ComponentChild as UiNode } from "preact";
import type { ExecutionState } from "../../domain/message-stream/items";
import type { AgentRunSummaryView, MessageStreamStatusView, StatusChecklistItem } from "../../presentation/message-stream/status-view";
import type { AgentRunSummaryView, MessageStreamStatusView } from "../../presentation/message-stream/status-view";
export function agentRunSummaryNode(view: AgentRunSummaryView): UiNode {
return <AgentRunSummary view={view} />;
@ -44,8 +44,19 @@ function TaskProgress({ view }: { view: Extract<MessageStreamStatusView, { kind:
) : (
<ul className="codex-panel__task-list">
{view.checklist.map((step) => (
<li key={`${step.status}\n${step.step}`} className={taskStepClassName(step.status)}>
<span className="codex-panel__task-marker">{taskStatusMarker(step.status)}</span>
<li
key={`${step.status}\n${step.step}`}
className={
step.status === "completed"
? "codex-panel__task-step codex-panel__task-step--completed"
: step.status === "inProgress"
? "codex-panel__task-step codex-panel__task-step--inProgress"
: "codex-panel__task-step"
}
>
<span className="codex-panel__task-marker">
{step.status === "completed" ? "[x]" : step.status === "inProgress" ? "[>]" : "[ ]"}
</span>
<span className="codex-panel__task-text">{step.step}</span>
</li>
))}
@ -55,18 +66,6 @@ function TaskProgress({ view }: { view: Extract<MessageStreamStatusView, { kind:
);
}
function taskStatusMarker(status: StatusChecklistItem["status"]): string {
if (status === "completed") return "[x]";
if (status === "inProgress") return "[>]";
return "[ ]";
}
function taskStepClassName(status: StatusChecklistItem["status"]): string {
if (status === "completed") return "codex-panel__task-step codex-panel__task-step--completed";
if (status === "inProgress") return "codex-panel__task-step codex-panel__task-step--inProgress";
return "codex-panel__task-step";
}
function ContextCompaction({ view }: { view: Extract<MessageStreamStatusView, { kind: "contextCompaction" }> }): UiNode {
return (
<StatusMessage label={view.label} className={view.className} state={view.state}>

View file

@ -206,7 +206,16 @@ function RateLimitPanel({ rateLimit }: { rateLimit: RateLimitSummary | null }):
<div className="codex-panel__limit-panel-title">Usage limit</div>
<div className="codex-panel__limit-panel-list">
{rateLimit.rows.map((row) => (
<div key={`${row.label}:${row.value}:${row.resetLabel ?? ""}`} className={rateLimitRowClassName(row.level)}>
<div
key={`${row.label}:${row.value}:${row.resetLabel ?? ""}`}
className={
row.level === "danger"
? "codex-panel__limit-panel-row codex-panel__limit-panel-row--danger"
: row.level === "warn"
? "codex-panel__limit-panel-row codex-panel__limit-panel-row--warn"
: "codex-panel__limit-panel-row"
}
>
<div className="codex-panel__limit-panel-label">{row.label}</div>
<div className="codex-panel__limit-panel-value">{row.value}</div>
<div className="codex-panel__limit-panel-meter-cell">
@ -214,7 +223,11 @@ function RateLimitPanel({ rateLimit }: { rateLimit: RateLimitSummary | null }):
className={[
"codex-panel__limit-panel-meter",
row.meterDivisions ? "codex-panel__limit-panel-meter--divided" : "",
rateLimitMeterDivisionClassName(row.meterDivisions),
row.meterDivisions === 5
? "codex-panel__limit-panel-meter--5"
: row.meterDivisions === 7
? "codex-panel__limit-panel-meter--7"
: "",
]
.filter(Boolean)
.join(" ")}
@ -265,32 +278,23 @@ function DiagnosticSection({ section }: { section: ToolbarDiagnosticSection }):
}
function DiagnosticRow({ row }: { row: ToolbarDiagnosticRow }): UiNode {
const level = row.level ?? "normal";
return (
<div className={diagnosticRowClassName(row.level ?? "normal")}>
<div
className={
level === "error"
? "codex-panel__connection-diagnostics-row codex-panel__connection-diagnostics-row--error"
: level === "warning"
? "codex-panel__connection-diagnostics-row codex-panel__connection-diagnostics-row--warning"
: "codex-panel__connection-diagnostics-row"
}
>
<dt>{row.label}</dt>
<dd>{row.value}</dd>
</div>
);
}
function rateLimitRowClassName(level: RateLimitSummary["rows"][number]["level"]): string {
if (level === "danger") return "codex-panel__limit-panel-row codex-panel__limit-panel-row--danger";
if (level === "warn") return "codex-panel__limit-panel-row codex-panel__limit-panel-row--warn";
return "codex-panel__limit-panel-row";
}
function rateLimitMeterDivisionClassName(divisions: RateLimitSummary["rows"][number]["meterDivisions"]): string {
if (divisions === 5) return "codex-panel__limit-panel-meter--5";
if (divisions === 7) return "codex-panel__limit-panel-meter--7";
return "";
}
function diagnosticRowClassName(level: NonNullable<ToolbarDiagnosticRow["level"]>): string {
if (level === "error") return "codex-panel__connection-diagnostics-row codex-panel__connection-diagnostics-row--error";
if (level === "warning") return "codex-panel__connection-diagnostics-row codex-panel__connection-diagnostics-row--warning";
return "codex-panel__connection-diagnostics-row";
}
function ThreadList({ threads, actions }: { threads: ToolbarThreadRow[]; actions: ToolbarThreadActions }): UiNode {
if (threads.length === 0) {
return (

View file

@ -62,7 +62,8 @@ function TurnDiffHeader({
<div className="codex-panel-chat-turn-diff__title-block">
<div className="codex-panel-chat-turn-diff__title">Turn diff</div>
<div className="codex-panel-chat-turn-diff__meta">
{shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} · {fileCountLabel(state.files)}
{shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} ·{" "}
{state.files.length === 1 ? "Edited 1 file" : `Edited ${String(state.files.length)} files`}
</div>
</div>
{copyDiff ? (
@ -84,7 +85,3 @@ function ChangedFiles({ files }: { files: string[] }): UiNode {
</details>
);
}
function fileCountLabel(files: string[]): string {
return files.length === 1 ? "Edited 1 file" : `Edited ${String(files.length)} files`;
}

View file

@ -55,7 +55,13 @@ export async function runSelectionRewrite(options: RunSelectionRewriteOptions):
timedOutMessage: "Timed out while rewriting the selection.",
abortMessage: "Selection rewrite cancelled.",
signal: options.signal,
resolveRuntime: runtimeSettings ? (client) => selectionRewriteRuntimeOverrideForClient(client, runtimeSettings) : undefined,
resolveRuntime: runtimeSettings
? (client) =>
resolvedRuntimeOverrideForClient(client, {
model: runtimeSettings.rewriteSelectionModel,
effort: runtimeSettings.rewriteSelectionEffort,
})
: undefined,
clientFactory: options.clientFactory,
onProgress: (event) => {
if (event.type === "reasoning-activity") {
@ -71,7 +77,3 @@ export async function runSelectionRewrite(options: RunSelectionRewriteOptions):
if (!output) throw new SelectionRewriteOutputError("Codex did not return a valid selection rewrite response.", rawText);
return output;
}
async function selectionRewriteRuntimeOverrideForClient(client: ModelMetadataClient, settings: SelectionRewriteRuntimeSettings) {
return resolvedRuntimeOverrideForClient(client, { model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort });
}

View file

@ -23,7 +23,7 @@ const THREAD_PICKER_MODIFIER_ENTER_LISTENER_OPTIONS = { capture: true } as const
export async function openThreadPicker(host: ThreadPickerHost): Promise<void> {
try {
const threads = await loadThreadPickerThreads(host);
const threads = await host.threadCatalog.loadActive();
if (threads.length === 0) {
new Notice("No Codex threads found.");
return;
@ -70,10 +70,6 @@ function threadOpenModeFromEvent(evt: MouseEvent | KeyboardEvent): ThreadOpenMod
return "current";
}
async function loadThreadPickerThreads(host: ThreadPickerHost): Promise<readonly Thread[]> {
return host.threadCatalog.loadActive();
}
class ThreadPickerModal extends SuggestModal<ThreadSuggestion> {
constructor(
private readonly host: ThreadPickerHost,

View file

@ -206,7 +206,7 @@ export class ThreadsViewSession {
renderThreadsViewShell(
this.environment.root,
{
status: threadsViewStatusText(this.status),
status: this.status.kind === "idle" ? null : this.status.message,
loading: this.refreshLifecycle.kind === "loading",
rows: threadRows(
this.threads,
@ -359,7 +359,3 @@ export class ThreadsViewSession {
return this.environment.viewWindow() ?? window;
}
}
function threadsViewStatusText(status: ThreadsViewStatus): string | null {
return status.kind === "idle" ? null : status.message;
}

View file

@ -51,10 +51,9 @@ export function threadRows(
.map((thread) => {
const threadSnapshots = snapshotsByThread.get(thread.id) ?? [];
const live = liveStateForSnapshots(threadSnapshots);
const selected = selectedStateForSnapshots(threadSnapshots);
const core = threadRowCoreProjection({
thread,
selected,
selected: threadSnapshots.some((snapshot) => snapshot.threadId !== null && snapshot.lastFocused),
renameState: renameStates.get(thread.id),
archiveConfirmActive: archiveConfirmThreadId === thread.id,
defaultArchiveSaveMarkdown,
@ -77,10 +76,6 @@ function liveStateForSnapshots(snapshots: OpenCodexPanelSnapshot[]): ThreadsLive
};
}
function selectedStateForSnapshots(snapshots: OpenCodexPanelSnapshot[]): boolean {
return snapshots.some((snapshot) => snapshot.threadId !== null && snapshot.lastFocused);
}
export function transitionThreadsRenameState(
state: ThreadsRenameLifecycleState,
event: ThreadsRenameLifecycleEvent,

View file

@ -15,26 +15,19 @@ import {
transitionSettingsDynamicSectionLifecycle,
} from "./lifecycle";
function archivedThreadTitleForStatus(thread: Thread | undefined, threadId: string): string {
return thread ? threadArchiveDisplayTitle(thread) : threadId;
}
interface LoadedHookCatalog extends HookCatalog {
status: string;
}
async function loadHookCatalog(client: AppServerClient, cwd: string): Promise<LoadedHookCatalog> {
const hooks = await listHookCatalog(client, cwd);
const hookCount = hooks.hooks.length;
return {
...hooks,
status: hooksStatus(hooks.hooks.length),
status: `Loaded ${String(hookCount)} hook${hookCount === 1 ? "" : "s"}.`,
};
}
function hooksStatus(count: number): string {
return `Loaded ${String(count)} hook${count === 1 ? "" : "s"}.`;
}
interface SettingsDynamicSectionsControllerCallbacks {
display(target: SettingsDynamicSectionsDisplayTarget): void;
notify(message: string): void;
@ -369,10 +362,8 @@ export class SettingsDynamicSectionsController {
}
async deleteArchivedThread(threadId: string): Promise<void> {
const title = archivedThreadTitleForStatus(
this.archivedThreads.find((thread) => thread.id === threadId),
threadId,
);
const thread = this.archivedThreads.find((item) => item.id === threadId);
const title = thread ? threadArchiveDisplayTitle(thread) : threadId;
await this.runDynamicSectionOperation({
section: "archivedThreads",
loadingStatus: "Loading archived threads...",

View file

@ -46,7 +46,7 @@ export function normalizeSettings(storedSettings: unknown): CodexPanelSettings {
rewriteSelectionModel: modelOrDefault(record["rewriteSelectionModel"], DEFAULT_SETTINGS.rewriteSelectionModel),
rewriteSelectionEffort: reasoningEffortOrDefault(record["rewriteSelectionEffort"], DEFAULT_SETTINGS.rewriteSelectionEffort),
showToolbar: booleanOrDefault(record["showToolbar"], DEFAULT_SETTINGS.showToolbar),
sendShortcut: sendShortcutOrDefault(record["sendShortcut"]),
sendShortcut: record["sendShortcut"] === "mod-enter" ? "mod-enter" : DEFAULT_SETTINGS.sendShortcut,
scrollThreadFromComposerEdges: booleanOrDefault(
record["scrollThreadFromComposerEdges"],
DEFAULT_SETTINGS.scrollThreadFromComposerEdges,
@ -90,10 +90,6 @@ function reasoningEffortOrDefault(value: unknown, fallback: ReasoningEffort | nu
return normalizeReasoningEffort(value) ?? fallback;
}
function sendShortcutOrDefault(value: unknown): SendShortcut {
return value === "mod-enter" ? "mod-enter" : DEFAULT_SETTINGS.sendShortcut;
}
export function getVaultPath(app: App): string {
const adapter = app.vault.adapter;
if (adapter instanceof FileSystemAdapter) {

View file

@ -11,7 +11,7 @@ let sourceSequence = 0;
export function createLocalIdSource(options: LocalIdSourceOptions = {}): LocalIdSource {
const nowMs = options.nowMs ?? Date.now;
const seed = sanitizeIdPart(options.seed ?? defaultIdSeed());
const seed = sanitizeIdPart(options.seed ?? Date.now().toString(36));
sourceSequence += 1;
const sourceId = `${seed}-${sourceSequence.toString(36)}`;
let itemSequence = 0;
@ -24,10 +24,6 @@ export function createLocalIdSource(options: LocalIdSourceOptions = {}): LocalId
};
}
function defaultIdSeed(): string {
return Date.now().toString(36);
}
function sanitizeIdPart(value: string): string {
return value.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 24) || "local";
}