Clarify runtime setting selection types

This commit is contained in:
murashit 2026-06-21 15:55:02 +09:00
parent 2e9d74167d
commit d0f37a2cae
10 changed files with 35 additions and 334 deletions

View file

@ -9,7 +9,7 @@ import {
type PendingRuntimeSettingsPatch,
} from "./thread-settings-update";
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
import { nextCollaborationMode, type CollaborationMode, type RequestedServiceTier } from "../../domain/runtime/pending-settings";
import { nextCollaborationMode, type CollaborationModeSelection, type RequestedServiceTier } from "../../domain/runtime/pending-settings";
import type { ChatAction, ChatState } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
@ -42,7 +42,7 @@ export interface ChatRuntimeSettingsActions {
disableFastMode: () => Promise<void>;
toggleFastMode: () => Promise<void>;
toggleCollaborationMode: () => Promise<void>;
setCollaborationMode: (collaborationMode: CollaborationMode) => Promise<boolean>;
setCollaborationMode: (collaborationMode: CollaborationModeSelection) => Promise<boolean>;
requestDefaultCollaborationModeForNextTurn: () => void;
enableAutoReview: () => Promise<void>;
disableAutoReview: () => Promise<void>;
@ -158,7 +158,7 @@ async function toggleCollaborationMode(host: RuntimeSettingsActionsHost): Promis
await setCollaborationMode(host, next);
}
async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborationMode: CollaborationMode): Promise<boolean> {
async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborationMode: CollaborationModeSelection): Promise<boolean> {
dispatch(host, { type: "runtime/requested-collaboration-mode-set", collaborationMode });
const result = await applyPendingThreadSettingsResult(host);
if (result.ok) dispatch(host, { type: "ui/panel-set", panel: null });
@ -197,7 +197,7 @@ function fastModeToggleMessage(state: FastModeState): string {
return state === "enabled" ? "Fast mode on for subsequent turns." : "Fast mode off for subsequent turns.";
}
function collaborationModeToggleMessage(mode: CollaborationMode): string {
function collaborationModeToggleMessage(mode: CollaborationModeSelection): string {
return mode === "plan" ? "Plan mode on for subsequent turns." : "Plan mode off for subsequent turns.";
}

View file

@ -51,10 +51,10 @@ export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: R
const runtimeCollaborationModeSettings = requestedTurnCollaborationModeSettings(snapshot, config);
if (snapshot.requestedModel.kind !== "unchanged") {
applyRuntimeSettingsPatchValue(update, "model", runtimeSettingsPatchValue(snapshot.requestedModel));
applyRuntimeSettingsPatchValue(update, "model", runtimeSettingsPatchValueFromPendingSetting(snapshot.requestedModel));
}
if (snapshot.requestedReasoningEffort.kind !== "unchanged") {
applyRuntimeSettingsPatchValue(update, "effort", runtimeSettingsPatchValue(snapshot.requestedReasoningEffort));
applyRuntimeSettingsPatchValue(update, "effort", runtimeSettingsPatchValueFromPendingSetting(snapshot.requestedReasoningEffort));
}
if (snapshot.requestedServiceTier.kind === "set") {
applyRuntimeSettingsPatchValue(
@ -66,7 +66,11 @@ export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: R
applyRuntimeSettingsPatchValue(update, "serviceTier", null);
}
if (snapshot.requestedApprovalsReviewer.kind !== "unchanged") {
applyRuntimeSettingsPatchValue(update, "approvalsReviewer", runtimeSettingsPatchValue(snapshot.requestedApprovalsReviewer));
applyRuntimeSettingsPatchValue(
update,
"approvalsReviewer",
runtimeSettingsPatchValueFromPendingSetting(snapshot.requestedApprovalsReviewer),
);
}
if (snapshot.selectedCollaborationMode !== effectiveCollaborationMode(snapshot.activeCollaborationMode)) {
if (runtimeCollaborationModeSettings.warning) {
@ -77,7 +81,7 @@ export function pendingRuntimeSettingsPatch(snapshot: RuntimeSnapshot, config: R
return { update, collaborationModeWarning: null };
}
function runtimeSettingsPatchValue<T>(setting: PendingRuntimeSetting<T>): T | null | undefined {
function runtimeSettingsPatchValueFromPendingSetting<T>(setting: PendingRuntimeSetting<T>): T | null | undefined {
if (setting.kind === "set") return setting.value;
if (setting.kind === "resetToConfig") return null;
return undefined;

View file

@ -4,7 +4,7 @@ import { upsertThread, type Thread } from "../../../../domain/threads/model";
import { parseServiceTier, type ServiceTier } from "../../../../domain/runtime/policy";
import { normalizeReasoningEffort, type ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { ChatRuntimeState } from "../../domain/runtime/state";
import type { CollaborationMode } from "../../domain/runtime/pending-settings";
import type { CollaborationModeSelection } from "../../domain/runtime/pending-settings";
import type { MessageStreamItem } from "../../domain/message-stream/items";
import type { PendingTurnStart } from "../conversation/turn-state";
@ -52,7 +52,7 @@ export interface ActiveThreadSettingsAppliedAction {
cwd: string;
model: string | null;
reasoningEffort: ReasoningEffort | null;
collaborationMode: CollaborationMode;
collaborationMode: CollaborationModeSelection;
serviceTier: ServiceTier | null;
approvalPolicy: ChatRuntimeState["activeApprovalPolicy"];
approvalsReviewer: ChatRuntimeState["activeApprovalsReviewer"];
@ -63,7 +63,7 @@ export interface ActiveThreadSettingsAppliedActionSettings {
cwd: string;
model: string | null;
effort: string | null;
collaborationMode: { mode: CollaborationMode };
collaborationMode: { mode: CollaborationModeSelection };
serviceTier: string | null;
approvalPolicy: ChatRuntimeState["activeApprovalPolicy"];
approvalsReviewer: ChatRuntimeState["activeApprovalsReviewer"];

View file

@ -9,7 +9,7 @@ import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
import type { RateLimitSnapshot, ThreadTokenUsage } from "../../../../domain/runtime/metrics";
import type { ApprovalsReviewer } from "../../../../domain/runtime/policy";
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
import type { CollaborationMode } from "../../domain/runtime/pending-settings";
import type { CollaborationModeSelection } from "../../domain/runtime/pending-settings";
import {
commitPendingRuntimeSettingsPatchState,
clearRequestedApprovalsReviewerRuntimeState,
@ -26,7 +26,7 @@ import {
type ChatRuntimeState,
} from "../../domain/runtime/state";
import type { RequestedServiceTier } from "../../domain/runtime/pending-settings";
import type { PendingRequestId } from "../../domain/pending-requests/model";
import type { PendingRequestId } from "../../../../domain/pending-requests/model";
import type { ComposerSuggestion } from "../composer/suggestions";
import type { MessageStreamItem } from "../../domain/message-stream/items";
import type {
@ -169,7 +169,7 @@ type RuntimeAction =
| { type: "runtime/service-tier-request-cleared" }
| { type: "runtime/approvals-reviewer-requested"; approvalsReviewer: ApprovalsReviewer }
| { type: "runtime/approvals-reviewer-request-cleared" }
| { type: "runtime/requested-collaboration-mode-set"; collaborationMode: CollaborationMode }
| { type: "runtime/requested-collaboration-mode-set"; collaborationMode: CollaborationModeSelection }
| { type: "runtime/pending-thread-settings-committed"; update: RuntimeSettingsPatch };
interface TurnStartedAction {

View file

@ -1,5 +1,5 @@
import type { ThreadGoal } from "../../../../domain/threads/goal";
import { pendingRequestDerivedKeyPrefix, type PendingRequestId } from "../../domain/pending-requests/model";
import { pendingRequestDerivedKeyPrefix, type PendingRequestId } from "../../../../domain/pending-requests/model";
import type { DisclosureSetAction } from "./actions";
export type ChatRenameUiState =

View file

@ -1,308 +0,0 @@
export type PendingRequestId = string | number;
type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
export type CommandApprovalDecision =
| SimpleApprovalDecision
| { acceptWithExecpolicyAmendment: unknown }
| { applyNetworkPolicyAmendment: { network_policy_amendment: { action: "allow" | "deny"; [key: string]: unknown } } };
interface CommandApprovalParams {
threadId: string;
turnId: string;
itemId: string;
startedAtMs: number;
approvalId?: string | null;
reason?: string | null;
networkApprovalContext?: unknown;
command?: string | null;
cwd?: string | null;
commandActions?: unknown[] | null;
additionalPermissions?: unknown;
proposedExecpolicyAmendment?: unknown;
proposedNetworkPolicyAmendments?: unknown[] | null;
availableDecisions?: CommandApprovalDecision[] | null;
}
interface FileChangeApprovalParams {
threadId: string;
turnId: string;
itemId: string;
startedAtMs: number;
reason: string | null;
grantRoot: string | null;
}
interface PermissionProfile {
network?: { enabled?: boolean | null } | null;
fileSystem?: {
entries?: readonly { path: unknown; access?: unknown }[] | null;
read?: unknown;
write?: unknown;
globScanMaxDepth?: unknown;
} | null;
}
interface PermissionsApprovalParams {
threadId: string;
turnId: string;
itemId: string;
startedAtMs: number;
reason: string | null;
cwd: string;
environmentId: string | null;
permissions: PermissionProfile;
}
export type ApprovalAction = "accept" | "accept-session" | "decline" | "cancel" | CommandApprovalDecisionAction;
interface CommandApprovalDecisionAction {
kind: "command-decision";
decision: CommandApprovalDecision;
}
export type PendingApproval =
| {
requestId: PendingRequestId;
method: "item/commandExecution/requestApproval";
params: CommandApprovalParams;
}
| {
requestId: PendingRequestId;
method: "item/fileChange/requestApproval";
params: FileChangeApprovalParams;
}
| {
requestId: PendingRequestId;
method: "item/permissions/requestApproval";
params: PermissionsApprovalParams;
};
interface PendingUserInputOption {
label: string;
description: string;
}
export interface PendingUserInputQuestion {
id: string;
header: string;
question: string;
isOther: boolean;
isSecret: boolean;
options: PendingUserInputOption[] | null;
}
interface PendingUserInputParams {
threadId: string;
turnId: string;
itemId: string;
questions: PendingUserInputQuestion[];
autoResolutionMs: number | null;
}
export interface PendingUserInput {
requestId: PendingRequestId;
method: "item/tool/requestUserInput";
params: PendingUserInputParams;
}
export type McpElicitationAction = "accept" | "decline" | "cancel";
export type McpElicitationContentValue = string | number | boolean | readonly string[] | null;
interface PendingMcpElicitationOption {
value: string;
label: string;
}
interface PendingMcpElicitationFieldBase {
id: string;
title: string;
description: string | null;
required: boolean;
}
export type PendingMcpElicitationField =
| (PendingMcpElicitationFieldBase & {
type: "string";
format: string | null;
minLength: number | null;
maxLength: number | null;
defaultValue: string;
})
| (PendingMcpElicitationFieldBase & {
type: "number" | "integer";
minimum: number | null;
maximum: number | null;
defaultValue: number | null;
})
| (PendingMcpElicitationFieldBase & {
type: "boolean";
defaultValue: boolean;
})
| (PendingMcpElicitationFieldBase & {
type: "single-select";
options: readonly PendingMcpElicitationOption[];
defaultValue: string;
})
| (PendingMcpElicitationFieldBase & {
type: "multi-select";
options: readonly PendingMcpElicitationOption[];
minItems: number | null;
maxItems: number | null;
defaultValue: readonly string[];
});
interface PendingMcpElicitationFormParams {
threadId: string;
turnId: string | null;
serverName: string;
mode: "form";
message: string;
meta: unknown;
fields: readonly PendingMcpElicitationField[];
}
interface PendingMcpElicitationUrlParams {
threadId: string;
turnId: string | null;
serverName: string;
mode: "url";
message: string;
meta: unknown;
url: string;
elicitationId: string;
}
export interface PendingMcpElicitation {
requestId: PendingRequestId;
method: "mcpServer/elicitation/request";
params: PendingMcpElicitationFormParams | PendingMcpElicitationUrlParams;
}
export function approvalActionKind(action: ApprovalAction): "accept" | "accept-session" | "decline" | "cancel" {
if (!isCommandDecisionAction(action)) return action;
const decision = action.decision;
if (typeof decision === "string") return simpleApprovalActionKind(decision);
if ("acceptWithExecpolicyAmendment" in decision) return "accept-session";
if ("applyNetworkPolicyAmendment" in decision) {
return decision.applyNetworkPolicyAmendment.network_policy_amendment.action === "allow" ? "accept-session" : "decline";
}
return "decline";
}
function simpleApprovalActionKind(decision: string): "accept" | "accept-session" | "decline" | "cancel" {
if (decision === "accept") return "accept";
if (decision === "acceptForSession") return "accept-session";
if (decision === "cancel") return "cancel";
return "decline";
}
function isCommandDecisionAction(action: ApprovalAction): action is CommandApprovalDecisionAction {
return typeof action === "object";
}
export function questionDefaultAnswer(question: PendingUserInputQuestion): string {
return question.options?.[0]?.label ?? "";
}
export function userInputDraftKey(requestId: PendingRequestId, questionId: string): string {
return pendingRequestDerivedKey(requestId, questionId);
}
export function userInputOtherDraftKey(requestId: PendingRequestId, questionId: string): string {
return pendingRequestDerivedKey(requestId, `${questionId}:other`);
}
export function mcpElicitationDraftKey(requestId: PendingRequestId, fieldId: string): string {
return pendingRequestDerivedKey(requestId, `mcp:${fieldId}`);
}
export function approvalDetailsDisclosureId(requestId: PendingRequestId): string {
return pendingRequestDerivedKey(requestId, "details");
}
export function pendingRequestDerivedKeyPrefix(requestId: PendingRequestId): string {
return `${String(requestId)}:`;
}
function pendingRequestDerivedKey(requestId: PendingRequestId, suffix: string): string {
return `${pendingRequestDerivedKeyPrefix(requestId)}${suffix}`;
}
export function answersForPendingUserInput(input: PendingUserInput, drafts: ReadonlyMap<string, string>): Record<string, string> {
return Object.fromEntries(
input.params.questions.map((question) => [
question.id,
drafts.get(userInputDraftKey(input.requestId, question.id)) ?? questionDefaultAnswer(question),
]),
);
}
export function contentForPendingMcpElicitation(
elicitation: PendingMcpElicitation,
drafts: ReadonlyMap<string, string>,
): Record<string, McpElicitationContentValue> | null {
if (elicitation.params.mode !== "form") return null;
return Object.fromEntries(
elicitation.params.fields.map((field) => [
field.id,
mcpElicitationFieldValue(field, drafts.get(mcpElicitationDraftKey(elicitation.requestId, field.id))),
]),
);
}
export function mcpElicitationFieldDefaultDraft(field: PendingMcpElicitationField): string {
switch (field.type) {
case "boolean":
return field.defaultValue ? "true" : "false";
case "multi-select":
return JSON.stringify(field.defaultValue);
case "number":
case "integer":
return field.defaultValue === null ? "" : String(field.defaultValue);
default:
return field.defaultValue;
}
}
function mcpElicitationFieldValue(field: PendingMcpElicitationField, draftValue: string | undefined): McpElicitationContentValue {
const draft = draftValue ?? mcpElicitationFieldDefaultDraft(field);
switch (field.type) {
case "boolean":
return draft === "true";
case "number":
case "integer":
return numericMcpElicitationFieldValue(field, draft);
case "multi-select":
return multiSelectMcpElicitationFieldValue(field, draft);
default:
return draft;
}
}
function numericMcpElicitationFieldValue(
field: Extract<PendingMcpElicitationField, { type: "number" | "integer" }>,
draft: string,
): number | null {
if (draft.trim() === "") return null;
const parsed = field.type === "integer" ? Number.parseInt(draft, 10) : Number(draft);
if (Number.isFinite(parsed)) return parsed;
return field.defaultValue;
}
function multiSelectMcpElicitationFieldValue(
field: Extract<PendingMcpElicitationField, { type: "multi-select" }>,
draft: string,
): readonly string[] {
try {
const parsed = JSON.parse(draft) as unknown;
if (Array.isArray(parsed)) {
const allowed = new Set(field.options.map((option) => option.value));
return parsed.filter((value): value is string => typeof value === "string" && allowed.has(value));
}
} catch {
// Fall through to schema default when a stale draft cannot be parsed.
}
return field.defaultValue;
}

View file

@ -1,5 +1,7 @@
export type CollaborationMode = "default" | "plan";
export type ActiveCollaborationMode = CollaborationMode | null;
import type { ModeKind } from "../../../../domain/runtime/thread-settings";
export type CollaborationModeSelection = ModeKind;
export type ActiveCollaborationMode = CollaborationModeSelection | null;
export type PendingRuntimeSetting<T> =
| { readonly kind: "unchanged" }
| { readonly kind: "set"; readonly value: T }
@ -18,10 +20,10 @@ export function resetRuntimeSettingToConfig<T>(): PendingRuntimeSetting<T> {
return { kind: "resetToConfig" };
}
export function nextCollaborationMode(mode: CollaborationMode): CollaborationMode {
export function nextCollaborationMode(mode: CollaborationModeSelection): CollaborationModeSelection {
return mode === "plan" ? "default" : "plan";
}
export function effectiveCollaborationMode(mode: ActiveCollaborationMode): CollaborationMode {
export function effectiveCollaborationMode(mode: ActiveCollaborationMode): CollaborationModeSelection {
return mode ?? "default";
}

View file

@ -2,7 +2,7 @@ import type { ModelMetadata, ReasoningEffort } from "../../../../domain/catalog/
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
import type { RateLimitSnapshot, ThreadTokenUsage } from "../../../../domain/runtime/metrics";
import type { ActivePermissionProfile, ApprovalPolicy, ApprovalsReviewer, ServiceTier } from "../../../../domain/runtime/policy";
import type { ActiveCollaborationMode, CollaborationMode, PendingRuntimeSetting, RequestedServiceTier } from "./pending-settings";
import type { ActiveCollaborationMode, CollaborationModeSelection, PendingRuntimeSetting, RequestedServiceTier } from "./pending-settings";
export interface RuntimeSnapshot {
runtimeConfig: RuntimeConfigSnapshot | null;
@ -17,7 +17,7 @@ export interface RuntimeSnapshot {
requestedModel: PendingRuntimeSetting<string>;
requestedReasoningEffort: PendingRuntimeSetting<ReasoningEffort>;
requestedApprovalsReviewer: PendingRuntimeSetting<ApprovalsReviewer>;
selectedCollaborationMode: CollaborationMode;
selectedCollaborationMode: CollaborationModeSelection;
requestedServiceTier: PendingRuntimeSetting<RequestedServiceTier>;
tokenUsage: ThreadTokenUsage | null;
rateLimit: RateLimitSnapshot | null;

View file

@ -12,7 +12,7 @@ import {
setPendingRuntimeSetting,
unchangedRuntimeSetting,
type ActiveCollaborationMode,
type CollaborationMode,
type CollaborationModeSelection,
type PendingRuntimeSetting,
type RequestedServiceTier,
} from "./pending-settings";
@ -28,7 +28,7 @@ export interface ChatRuntimeState {
readonly requestedModel: PendingRuntimeSetting<string>;
readonly requestedReasoningEffort: PendingRuntimeSetting<ReasoningEffort>;
readonly requestedApprovalsReviewer: PendingRuntimeSetting<ApprovalsReviewer>;
readonly selectedCollaborationMode: CollaborationMode;
readonly selectedCollaborationMode: CollaborationModeSelection;
readonly requestedServiceTier: PendingRuntimeSetting<RequestedServiceTier>;
}
@ -120,7 +120,10 @@ export function clearRequestedApprovalsReviewerRuntimeState(state: ChatRuntimeSt
};
}
export function setSelectedCollaborationModeRuntimeState(state: ChatRuntimeState, collaborationMode: CollaborationMode): ChatRuntimeState {
export function setSelectedCollaborationModeRuntimeState(
state: ChatRuntimeState,
collaborationMode: CollaborationModeSelection,
): ChatRuntimeState {
return {
...state,
selectedCollaborationMode: collaborationMode,

View file

@ -1,5 +1,5 @@
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { CollaborationMode } from "../../domain/runtime/pending-settings";
import type { CollaborationModeSelection } from "../../domain/runtime/pending-settings";
export function compactReasoningEffortLabel(effort: ReasoningEffort | null): string {
if (!effort) return "default";
@ -7,7 +7,7 @@ export function compactReasoningEffortLabel(effort: ReasoningEffort | null): str
return effort;
}
export function collaborationModeLabel(mode: CollaborationMode): string {
export function collaborationModeLabel(mode: CollaborationModeSelection): string {
return mode === "plan" ? "Plan" : "Default";
}