Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

423 lines
16 KiB
TypeScript

import type {
AgentActivityComposerOptionsLoadStatus,
AgentActivityComposerOptions,
AgentActivitySession
} from "@tutti-os/agent-activity-core";
import { useMemo } from "react";
import { areWorkspaceUserProjectPathsEqual } from "@tutti-os/workspace-user-project/core";
import type { AgentGUIRuntime } from "../../../agentActivityRuntime";
import type {
AgentSessionComposerSettings,
AgentSessionReasoningEffort,
AgentSessionSpeed,
AgentSessionState
} from "../../../shared/agentSessionTypes";
import type { AgentGUINodeData, AgentGUIProvider } from "../../../types";
import type { AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import type { AgentGUIConversationUserProject } from "../model/agentGuiConversationProjectResolver";
import type { AgentGUIComposerSettingsVM } from "../model/agentGuiNodeTypes";
import { slashCommandPolicyFromComposerOptions } from "../model/agentSlashCommandProviderPolicy";
import { composerSettingsSupportFromOptions } from "../model/composerSettingsSupport";
import {
cloneComposerSettings,
modelSelectionFromComposerOptions,
normalizePermissionModeId,
permissionConfigFromComposerOptions,
permissionModeOptions,
readNodeDefaultDraftSettings,
reasoningSelectionFromComposerOptions,
speedSelectionFromComposerOptions
} from "./agentGuiController.composerHelpers";
import {
composerModelChoiceHistoryTargetId,
enforceComposerModelBindingForHomeDefaults,
isForegroundModelOptionsLoading,
resolveComposerSettingsPresentation,
sanitizeComposerSettingsForTarget,
type AgentGUIComposerTargetData
} from "./agentGuiController.composerPresentation";
import { normalizeOptionalText } from "./agentGuiController.promptHelpers";
import { overlayComposerDefaults } from "./agentGuiController.providerHelpers";
import {
useStableComposerSettings,
useStableComposerSettingsVM
} from "./agentGuiController.stableHelpers";
interface UseAgentGUIComposerPresentationInput {
activeConversation: AgentGUIConversationSummary | null;
activeConversationId: string | null;
activeEngineSession: Pick<AgentActivitySession, "settings"> | null;
activeSessionState: AgentSessionState | null;
agentActivityRuntime: AgentGUIRuntime;
composerSupport: ReturnType<typeof composerSettingsSupportFromOptions>;
composerOptionsLoadStatus?: AgentActivityComposerOptionsLoadStatus;
composerOptionsLoading: boolean;
connectorOptionsLoading: boolean;
composerTargetProvider: AgentGUIProvider;
codexSaverModeEntryEnabled?: boolean;
rtkSaverModeEntryEnabled?: boolean;
data: AgentGUINodeData;
defaultReasoningEffort: AgentSessionReasoningEffort | null;
draftSettingsBySessionId: Record<string, AgentSessionComposerSettings>;
providerComposerOptions: AgentActivityComposerOptions | null;
composerTargetData: AgentGUIComposerTargetData;
selectedComposerTargetData: AgentGUIComposerTargetData;
selectedProjectPath: string | null;
shouldApplyPreparedProjectSelection: boolean;
userProjects: readonly AgentGUIConversationUserProject[];
}
export function useAgentGUIComposerPresentation(
input: UseAgentGUIComposerPresentationInput
) {
const sessionSettings = useStableComposerSettings(
cloneComposerSettings(input.activeSessionState?.settings ?? null)
);
const storedNodeDefaultSettings = useStableComposerSettings(
readNodeDefaultDraftSettings({
data:
input.activeConversationId === null
? input.selectedComposerTargetData.data
: input.data,
defaultReasoningEffort: input.defaultReasoningEffort,
drafts: input.draftSettingsBySessionId
})
);
// Home defaults additionally refuse bare models the settled provider list
// rejects (a plan model leaked bare into a provider bucket must fall back
// to the provider default, not surface as the composer default). The
// composer-options authority reconciliation persists the same correction,
// which also stops the rejected model from riding along in later requests.
const targetSafeNodeDefaultSettings = useStableComposerSettings(
input.activeConversationId === null
? enforceComposerModelBindingForHomeDefaults(
sanitizeComposerSettingsForTarget({
settings: storedNodeDefaultSettings,
target: input.selectedComposerTargetData,
options: input.providerComposerOptions
}),
input.providerComposerOptions
)
: storedNodeDefaultSettings
);
const homeComposerSettings = useStableComposerSettings(
resolveComposerSettingsPresentation({
active: false,
homeSettings: targetSafeNodeDefaultSettings,
options: input.providerComposerOptions
})
);
const activeConversationDraftSettings = input.activeConversationId
? (input.draftSettingsBySessionId[input.activeConversationId] ?? null)
: null;
const presentedDraftSettings = resolveComposerSettingsPresentation({
active: input.activeConversationId !== null,
homeSettings: homeComposerSettings,
optimisticSettings: activeConversationDraftSettings,
options: input.providerComposerOptions,
permissionModeId: input.activeSessionState?.permissionModeId,
sessionSettings
});
// Layer resolution can resurrect a rejected bare model from the preloaded
// daemon effective settings (per-target prefs poisoned by the same leak),
// so the home policy is enforced on the merged result, not just the node
// defaults.
const draftSettings = useStableComposerSettings(
input.activeConversationId === null
? enforceComposerModelBindingForHomeDefaults(
presentedDraftSettings,
input.providerComposerOptions
)
: presentedDraftSettings
);
const persistedDraftModel = normalizeOptionalText(draftSettings.model);
const usesPlaceholderDraftModel =
persistedDraftModel === null || persistedDraftModel === "default";
const liveConfigModel =
input.activeConversationId !== null && usesPlaceholderDraftModel
? normalizeOptionalText(input.activeEngineSession?.settings?.model)
: null;
const draftModel = usesPlaceholderDraftModel
? (liveConfigModel ?? persistedDraftModel)
: persistedDraftModel;
const draftReasoningEffort = normalizeOptionalText(
draftSettings.reasoningEffort
) as AgentSessionReasoningEffort | null;
const draftSpeed = normalizeOptionalText(
draftSettings.speed
) as AgentSessionSpeed | null;
const activeSessionReasoningSelection = useMemo(
() =>
reasoningSelectionFromComposerOptions(
input.providerComposerOptions,
draftReasoningEffort,
draftModel
),
[draftModel, draftReasoningEffort, input.providerComposerOptions]
);
const optionsReasoningEffort = activeSessionReasoningSelection
? activeSessionReasoningSelection.currentValue
: draftReasoningEffort;
const activeSessionModelSelection = useMemo(
() =>
modelSelectionFromComposerOptions(
input.providerComposerOptions,
draftModel
),
[draftModel, input.providerComposerOptions]
);
const activeSessionSpeedSelection = useMemo(
() =>
speedSelectionFromComposerOptions(
input.providerComposerOptions,
draftSpeed
),
[draftSpeed, input.providerComposerOptions]
);
const composerSettings = useMemo<AgentGUIComposerSettingsVM>(() => {
const permissionConfig = permissionConfigFromComposerOptions(
input.providerComposerOptions
);
const supportsPermissionMode = Boolean(
permissionConfig?.configurable && permissionConfig.modes.length > 0
);
const hasOptionsSource = input.providerComposerOptions !== null;
const hasACPSettings =
hasOptionsSource &&
(!input.composerSupport.model || activeSessionModelSelection !== null) &&
(!input.composerSupport.reasoning ||
activeSessionReasoningSelection !== null);
const composerOptionsError =
input.composerOptionsLoadStatus === "error" && !hasOptionsSource;
const selectedPermissionModeValue =
normalizePermissionModeId(draftSettings.permissionModeId) ??
normalizePermissionModeId(permissionConfig?.defaultValue);
const protectedSettings = overlayComposerDefaults(
{
codexSaverMode:
input.providerComposerOptions?.effectiveSettings?.codexSaverMode ===
true,
rtkSaverMode:
input.providerComposerOptions?.effectiveSettings?.rtkSaverMode ===
true,
model: draftModel,
permissionModeId: selectedPermissionModeValue,
reasoningEffort: optionsReasoningEffort,
speed: draftSpeed
},
input.activeConversationId === null
? input.selectedComposerTargetData.agentTargetId
? storedNodeDefaultSettings
: null
: sessionSettings
);
const presentedModel = normalizeOptionalText(protectedSettings.model);
const presentedReasoningEffort = normalizeOptionalText(
protectedSettings.reasoningEffort
) as AgentSessionReasoningEffort | null;
const presentedSpeed = normalizeOptionalText(
protectedSettings.speed
) as AgentSessionSpeed | null;
const presentedPermissionMode = normalizePermissionModeId(
protectedSettings.permissionModeId
);
return {
sessionSettings,
draftSettings: {
codexSaverMode:
input.codexSaverModeEntryEnabled === true &&
input.providerComposerOptions?.codexSaverModeSupported === true &&
protectedSettings.codexSaverMode === true,
rtkSaverMode:
input.rtkSaverModeEntryEnabled === true &&
input.providerComposerOptions?.rtkSaverModeSupported === true &&
protectedSettings.rtkSaverMode === true,
model: presentedModel,
reasoningEffort: presentedReasoningEffort,
speed: presentedSpeed,
planMode: Boolean(draftSettings.planMode),
browserUse: draftSettings.browserUse ?? true,
computerUse: draftSettings.computerUse ?? true,
permissionModeId: presentedPermissionMode
},
supportsModel: input.composerSupport.model,
supportsCodexSaverMode:
input.codexSaverModeEntryEnabled &&
input.providerComposerOptions?.codexSaverModeSupported === true,
supportsRTKSaverMode:
input.rtkSaverModeEntryEnabled &&
input.providerComposerOptions?.rtkSaverModeSupported === true,
supportsReasoningEffort: input.composerSupport.reasoning,
supportsSpeed: input.composerSupport.speed,
supportsBrowser: input.composerSupport.browser,
supportsComputerUse: input.composerSupport.computer,
permissionModeChangeDuringTurn:
input.composerSupport.permissionModeChangeDuringTurn,
slashCommandPolicy: slashCommandPolicyFromComposerOptions(
input.providerComposerOptions
),
supportsPermissionMode,
supportsPlanMode: input.composerSupport.plan,
planExclusiveWithPermissionMode:
input.providerComposerOptions?.behavior
?.planModeExclusiveWithPermissionMode === true,
composerOptionsError,
composerOptionsLoadStatus: input.composerOptionsLoadStatus,
isSettingsLoading: !hasACPSettings && !composerOptionsError,
isCapabilityOptionsLoading: input.composerOptionsLoading,
isConnectorOptionsLoading: input.connectorOptionsLoading,
isModelOptionsLoading: isForegroundModelOptionsLoading({
modelOptionsLoading: input.providerComposerOptions?.modelOptionsLoading,
selection: activeSessionModelSelection,
supportsModel: input.composerSupport.model
}),
modelChoiceHistory: {
targetId: composerModelChoiceHistoryTargetId({
activeConversationId: input.activeConversationId,
target: input.composerTargetData
}),
catalog: input.providerComposerOptions
? {
authoritative:
input.providerComposerOptions.behavior
.modelOptionsAuthoritative === true,
loading:
input.providerComposerOptions.modelOptionsLoading === true,
effectiveModel: normalizeOptionalText(
input.providerComposerOptions.effectiveSettings?.model
),
models: input.providerComposerOptions.models.map((option) => ({
value: option.value,
...(option.requested === true ? { requested: true } : {})
}))
}
: null
},
modelUnavailable:
input.activeConversationId !== null &&
sessionSettings === null &&
input.composerSupport.model &&
draftModel === null,
reasoningUnavailable:
input.activeConversationId !== null &&
sessionSettings === null &&
input.composerSupport.reasoning &&
draftReasoningEffort === null,
speedUnavailable:
input.activeConversationId !== null &&
sessionSettings === null &&
input.composerSupport.speed &&
draftSpeed === null,
permissionModeUnavailable:
input.activeConversationId !== null &&
sessionSettings === null &&
supportsPermissionMode &&
selectedPermissionModeValue === null,
selectedModelValue: presentedModel,
effectiveModelValue:
input.providerComposerOptions?.effectiveModel ?? null,
selectedReasoningEffortValue: presentedReasoningEffort,
selectedSpeedValue: presentedSpeed,
selectedPermissionModeValue: presentedPermissionMode,
permissionConfig,
selectedProjectPath:
input.activeConversationId !== null
? (input.activeConversation?.cwd ?? null)
: input.selectedProjectPath,
selectedProjectSectionKey:
input.activeConversationId !== null
? (input.activeConversation?.railSectionKey ?? null)
: resolveSelectedProjectSectionKey(
input.selectedProjectPath,
input.userProjects
),
shouldApplyPreparedProjectSelection:
input.activeConversationId === null &&
input.shouldApplyPreparedProjectSelection,
projectLocked: input.activeConversationId !== null,
projectPathIsRemote: input.agentActivityRuntime.projectPathIsRemote,
collapseModelOptionsToLatest:
input.providerComposerOptions?.behavior.collapseModelOptionsToLatest ===
true,
modelPlan: input.providerComposerOptions?.modelPlan ?? null,
modelSwitchTakesEffectNextTurn:
input.activeConversationId !== null &&
input.composerSupport.modelSwitch,
availableModels:
input.composerSupport.model &&
hasOptionsSource &&
activeSessionModelSelection !== null
? activeSessionModelSelection.options
: [],
availableReasoningEfforts:
input.composerSupport.reasoning &&
hasOptionsSource &&
activeSessionReasoningSelection !== null
? activeSessionReasoningSelection.options
: [],
availableSpeeds:
input.composerSupport.speed &&
hasOptionsSource &&
activeSessionSpeedSelection !== null
? activeSessionSpeedSelection.options
: [],
availablePermissionModes: supportsPermissionMode
? permissionModeOptions(input.composerTargetProvider, permissionConfig)
: []
};
}, [
activeSessionModelSelection,
activeSessionReasoningSelection,
activeSessionSpeedSelection,
draftModel,
draftReasoningEffort,
draftSettings,
draftSpeed,
input.activeConversation?.cwd,
input.activeConversation?.railSectionKey,
input.activeConversationId,
input.agentActivityRuntime.projectPathIsRemote,
input.composerSupport,
input.composerOptionsLoadStatus,
input.composerOptionsLoading,
input.connectorOptionsLoading,
input.composerTargetData,
input.composerTargetProvider,
input.providerComposerOptions,
input.selectedComposerTargetData.agentTargetId,
input.selectedProjectPath,
input.shouldApplyPreparedProjectSelection,
input.userProjects,
optionsReasoningEffort,
sessionSettings,
storedNodeDefaultSettings
]);
return {
draftModel,
draftReasoningEffort,
draftSettings,
draftSpeed,
sessionSettings,
stableComposerSettings: useStableComposerSettingsVM(composerSettings)
};
}
function resolveSelectedProjectSectionKey(
selectedProjectPath: string | null,
userProjects: readonly AgentGUIConversationUserProject[]
): string | null {
const projectPath = selectedProjectPath?.trim() ?? "";
if (!projectPath) {
return "conversations";
}
return (
userProjects
.find((project) =>
areWorkspaceUserProjectPathsEqual(project.path, projectPath)
)
?.sectionKey?.trim() || null
);
}