From d0f37a2cae572ea3f69c1e22c3376e2a57d87bb7 Mon Sep 17 00:00:00 2001 From: murashit Date: Sun, 21 Jun 2026 15:55:02 +0900 Subject: [PATCH] Clarify runtime setting selection types --- .../application/runtime/settings-actions.ts | 8 +- .../runtime/thread-settings-update.ts | 12 +- .../chat/application/state/actions.ts | 6 +- .../chat/application/state/root-reducer.ts | 6 +- .../chat/application/state/ui-state.ts | 2 +- .../chat/domain/pending-requests/model.ts | 308 ------------------ .../chat/domain/runtime/pending-settings.ts | 10 +- src/features/chat/domain/runtime/snapshot.ts | 4 +- src/features/chat/domain/runtime/state.ts | 9 +- .../chat/presentation/runtime/messages.ts | 4 +- 10 files changed, 35 insertions(+), 334 deletions(-) delete mode 100644 src/features/chat/domain/pending-requests/model.ts diff --git a/src/features/chat/application/runtime/settings-actions.ts b/src/features/chat/application/runtime/settings-actions.ts index 9b711303..d45d2b4f 100644 --- a/src/features/chat/application/runtime/settings-actions.ts +++ b/src/features/chat/application/runtime/settings-actions.ts @@ -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; toggleFastMode: () => Promise; toggleCollaborationMode: () => Promise; - setCollaborationMode: (collaborationMode: CollaborationMode) => Promise; + setCollaborationMode: (collaborationMode: CollaborationModeSelection) => Promise; requestDefaultCollaborationModeForNextTurn: () => void; enableAutoReview: () => Promise; disableAutoReview: () => Promise; @@ -158,7 +158,7 @@ async function toggleCollaborationMode(host: RuntimeSettingsActionsHost): Promis await setCollaborationMode(host, next); } -async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborationMode: CollaborationMode): Promise { +async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborationMode: CollaborationModeSelection): Promise { 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."; } diff --git a/src/features/chat/application/runtime/thread-settings-update.ts b/src/features/chat/application/runtime/thread-settings-update.ts index 40690785..96dbc784 100644 --- a/src/features/chat/application/runtime/thread-settings-update.ts +++ b/src/features/chat/application/runtime/thread-settings-update.ts @@ -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(setting: PendingRuntimeSetting): T | null | undefined { +function runtimeSettingsPatchValueFromPendingSetting(setting: PendingRuntimeSetting): T | null | undefined { if (setting.kind === "set") return setting.value; if (setting.kind === "resetToConfig") return null; return undefined; diff --git a/src/features/chat/application/state/actions.ts b/src/features/chat/application/state/actions.ts index 72b0eaa5..2aa72459 100644 --- a/src/features/chat/application/state/actions.ts +++ b/src/features/chat/application/state/actions.ts @@ -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"]; diff --git a/src/features/chat/application/state/root-reducer.ts b/src/features/chat/application/state/root-reducer.ts index 66e22912..84a686b9 100644 --- a/src/features/chat/application/state/root-reducer.ts +++ b/src/features/chat/application/state/root-reducer.ts @@ -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 { diff --git a/src/features/chat/application/state/ui-state.ts b/src/features/chat/application/state/ui-state.ts index 16256ff2..c04b2f10 100644 --- a/src/features/chat/application/state/ui-state.ts +++ b/src/features/chat/application/state/ui-state.ts @@ -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 = diff --git a/src/features/chat/domain/pending-requests/model.ts b/src/features/chat/domain/pending-requests/model.ts deleted file mode 100644 index fcbee56b..00000000 --- a/src/features/chat/domain/pending-requests/model.ts +++ /dev/null @@ -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): Record { - 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, -): Record | 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, - 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, - 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; -} diff --git a/src/features/chat/domain/runtime/pending-settings.ts b/src/features/chat/domain/runtime/pending-settings.ts index eb083570..90792fec 100644 --- a/src/features/chat/domain/runtime/pending-settings.ts +++ b/src/features/chat/domain/runtime/pending-settings.ts @@ -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 = | { readonly kind: "unchanged" } | { readonly kind: "set"; readonly value: T } @@ -18,10 +20,10 @@ export function resetRuntimeSettingToConfig(): PendingRuntimeSetting { 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"; } diff --git a/src/features/chat/domain/runtime/snapshot.ts b/src/features/chat/domain/runtime/snapshot.ts index 281120a7..b6dc5971 100644 --- a/src/features/chat/domain/runtime/snapshot.ts +++ b/src/features/chat/domain/runtime/snapshot.ts @@ -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; requestedReasoningEffort: PendingRuntimeSetting; requestedApprovalsReviewer: PendingRuntimeSetting; - selectedCollaborationMode: CollaborationMode; + selectedCollaborationMode: CollaborationModeSelection; requestedServiceTier: PendingRuntimeSetting; tokenUsage: ThreadTokenUsage | null; rateLimit: RateLimitSnapshot | null; diff --git a/src/features/chat/domain/runtime/state.ts b/src/features/chat/domain/runtime/state.ts index e0ef81ac..34adebb2 100644 --- a/src/features/chat/domain/runtime/state.ts +++ b/src/features/chat/domain/runtime/state.ts @@ -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; readonly requestedReasoningEffort: PendingRuntimeSetting; readonly requestedApprovalsReviewer: PendingRuntimeSetting; - readonly selectedCollaborationMode: CollaborationMode; + readonly selectedCollaborationMode: CollaborationModeSelection; readonly requestedServiceTier: PendingRuntimeSetting; } @@ -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, diff --git a/src/features/chat/presentation/runtime/messages.ts b/src/features/chat/presentation/runtime/messages.ts index f7e16bc7..4fa9c157 100644 --- a/src/features/chat/presentation/runtime/messages.ts +++ b/src/features/chat/presentation/runtime/messages.ts @@ -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"; }