Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUINewConversationActivation.ts
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

486 lines
16 KiB
TypeScript

import {
type AgentActivityComposerOptions,
type AgentActivityInitialGoalControl,
type AgentActivityRailPlacement,
isPendingActivationViable,
selectLatestActivationForSession,
selectTuttiModeDraftIsActive,
selectTuttiModeDraftPreferences
} from "@tutti-os/agent-activity-core";
import { useCallback } from "react";
import { translate } from "../../../i18n/index";
import type { AgentPromptContentBlock } from "../../../shared/contracts/dto";
import type { AgentSessionComposerSettings } from "../../../shared/agentSessionTypes";
import { deriveAgentGUIOptimisticConversationTitle } from "../../../shared/agentConversationTitleProjection";
import {
agentPromptContentDisplayText,
emptyAgentComposerDraft,
normalizeAgentPromptContentBlocks,
snapshotAgentComposerDraft,
textPromptContent
} from "../model/agentComposerDraft";
import type { AgentComposerSubmitOptions } from "../composer/AgentComposer.types";
import { resolveAgentComposerDraftScopeKey } from "../model/agentComposerDraftScope";
import {
type AgentGUIConversationUserProject,
resolveAgentGUISelectedUserProject
} from "../model/agentGuiConversationProjectResolver";
import {
normalizePermissionModeId,
permissionConfigFromComposerOptions,
readNodeDefaultDraftSettings
} from "./agentGuiController.composerHelpers";
import { effectiveComposerSettingsFromOptions } from "./agentGuiController.composerPresentation";
import { toRuntimeSendContent } from "./agentGuiController.draftMessageHelpers";
import {
createAgentGUIConversationId,
normalizeOptionalPrompt,
normalizeOptionalText
} from "./agentGuiController.promptHelpers";
import {
agentSubmitTraceDiagnostics,
createAgentSubmitTraceState,
reportAgentSubmitTraceDiagnostic
} from "./agentGuiController.reporting";
import { draftAgentSessionIdFromComposerOptions } from "./agentGuiController.stableHelpers";
import {
type AgentGUINewConversationActivationResult,
type UseAgentGUINewConversationActivationInput
} from "./agentGuiNewConversationActivation.types";
interface ResolvedInitialTuttiModeActivation {
activation: {
source: "slash_command";
status: "active";
effect?: number;
speed?: number;
};
source: "composer_submit" | "engine_draft";
}
export function resolveInitialTuttiModeActivation(input: {
submitOptions?: AgentComposerSubmitOptions;
draftActive: boolean;
draftEffect: number | null;
draftSpeed: number | null;
}): ResolvedInitialTuttiModeActivation | null {
const submitSnapshot = input.submitOptions?.tuttiMode;
const active = submitSnapshot?.active ?? input.draftActive;
if (!active) return null;
const effect = normalizePreference(
submitSnapshot?.effect ?? input.draftEffect
);
const speed = normalizePreference(submitSnapshot?.speed ?? input.draftSpeed);
return {
activation: {
source: "slash_command",
status: "active",
...(effect === null ? {} : { effect }),
...(speed === null ? {} : { speed })
},
source: submitSnapshot ? "composer_submit" : "engine_draft"
};
}
function normalizePreference(value: number | null | undefined) {
if (typeof value !== "number" || !Number.isFinite(value)) return null;
return Math.min(100, Math.max(0, Math.round(value)));
}
function firstResolvedComposerText(
...values: Array<string | null | undefined>
): string | null {
for (const value of values) {
const normalized = normalizeOptionalText(value);
if (normalized) {
return normalized;
}
}
return null;
}
/**
* Resolve the sparse Create payload to match the home composer presentation.
*
* Remembered defaults can retire out of the optimistic draft after authority
* confirmation while the UI still shows them via effectiveSettings /
* permissionConfig.defaultValue. Create must send those presented values so
* the first turn does not fall back to the provider default (for Codex: auto /
* "Approve for me").
*/
export function resolveSparseNewConversationActivationSettings(input: {
draftSettings: AgentSessionComposerSettings;
composerOptions: AgentActivityComposerOptions | null;
requiredSettingsPatch?: Partial<AgentSessionComposerSettings> | null;
codexSaverModeEntryEnabled?: boolean;
rtkSaverModeEntryEnabled?: boolean;
}): AgentSessionComposerSettings {
const draft = input.draftSettings ?? {};
const patch = input.requiredSettingsPatch ?? {};
const effective = effectiveComposerSettingsFromOptions(input.composerOptions);
const permissionConfig = permissionConfigFromComposerOptions(
input.composerOptions
);
const model = firstResolvedComposerText(
typeof patch.model === "string" ? patch.model : null,
draft.model,
effective?.model
);
const explicitReasoningEffort = firstResolvedComposerText(
typeof patch.reasoningEffort === "string" ? patch.reasoningEffort : null
);
const inheritedReasoningEffort = firstResolvedComposerText(
draft.reasoningEffort,
effective?.reasoningEffort
);
const reasoningEffort =
explicitReasoningEffort ??
resolveInheritedReasoningEffortForModel(
input.composerOptions,
model,
inheritedReasoningEffort
);
const speed = firstResolvedComposerText(
typeof patch.speed === "string" ? patch.speed : null,
draft.speed,
effective?.speed
);
const permissionModeId = firstResolvedComposerText(
typeof patch.permissionModeId === "string" ? patch.permissionModeId : null,
draft.permissionModeId,
effective?.permissionModeId,
permissionConfig?.defaultValue
);
return {
...draft,
...patch,
...(model ? { model } : {}),
reasoningEffort: reasoningEffort ?? undefined,
...(speed ? { speed } : {}),
...(permissionModeId
? { permissionModeId: normalizePermissionModeId(permissionModeId) }
: {}),
// Fail closed at the activation boundary. Presentation gating alone is
// insufficient because a remembered draft can outlive the lab flag.
codexSaverMode:
input.codexSaverModeEntryEnabled === true &&
input.composerOptions?.codexSaverModeSupported === true &&
(patch.codexSaverMode ??
draft.codexSaverMode ??
input.composerOptions.effectiveSettings?.codexSaverMode) === true,
rtkSaverMode:
input.rtkSaverModeEntryEnabled === true &&
input.composerOptions?.rtkSaverModeSupported === true &&
(patch.rtkSaverMode ??
draft.rtkSaverMode ??
input.composerOptions.effectiveSettings?.rtkSaverMode) === true
};
}
export function resolveNewConversationSettingProvenance(
requiredSettingsPatch?: Partial<AgentSessionComposerSettings> | null
): { modelExplicit: boolean; reasoningEffortExplicit: boolean } {
return {
modelExplicit: typeof requiredSettingsPatch?.model === "string",
reasoningEffortExplicit:
typeof requiredSettingsPatch?.reasoningEffort === "string"
};
}
function resolveInheritedReasoningEffortForModel(
options: AgentActivityComposerOptions | null,
model: string | null,
selected: string | null
): string | null {
if (options?.provider?.trim().toLowerCase() !== "opencode" || !model) {
return selected;
}
const profile = options.reasoningOptionsByModel?.[model];
if (!profile) {
// OpenCode's strict catalog is authoritative. Missing per-model metadata
// means there is no safe inherited dependent value to forward.
return null;
}
const supported = new Set(profile.options.map((option) => option.value));
if (selected && supported.has(selected)) return selected;
const advertisedDefault = profile.defaultValue?.trim() ?? "";
if (advertisedDefault && supported.has(advertisedDefault)) {
return advertisedDefault;
}
return profile.options[0]?.value ?? null;
}
export function resolveInitialRailPlacement(input: {
selectedProjectPath: string | null | undefined;
userProjects: readonly AgentGUIConversationUserProject[];
}): AgentActivityRailPlacement | null {
const selectedProjectPath = input.selectedProjectPath?.trim() ?? "";
if (!selectedProjectPath) {
return {
version: 1,
kind: "conversations",
sectionKey: "conversations"
};
}
const selectedProject = resolveAgentGUISelectedUserProject(
selectedProjectPath,
input.userProjects
);
if (!selectedProject) {
return null;
}
const sectionKey = selectedProject.sectionKey?.trim() ?? "";
if (!sectionKey) {
return null;
}
return {
version: 1,
kind: "project",
projectPath: selectedProject.path.trim(),
sectionKey
};
}
export function useAgentGUINewConversationActivation(
input: UseAgentGUINewConversationActivationInput
) {
const {
codexSaverModeEntryEnabled,
rtkSaverModeEntryEnabled,
getCachedComposerOptions,
selectedAgentTargetRef,
selectedComposerTargetDataRef,
agentTargetsProvidedRef,
selectedAgentTargetIsExplicitRef,
setDetailError,
isCreatingConversationRef,
onDataChangeRef,
selectedProjectPathRef,
userProjectsRef,
draftByScopeKeyRef,
submittedDraftSnapshotsRef,
draftSettingsBySessionIdRef,
agentActivityRuntime,
workspaceId,
activeConversationIdRef,
isComposerHomeRef,
activeSessionState,
sessionEngine,
tuttiModeDraftKey,
activation,
currentUserId,
data,
defaultReasoningEffort,
requestRailReveal,
setActiveConversationId,
setIntent,
setIsComposerHome,
setIsLoadingMessages,
conversationListQuery,
isCurrentConversation,
isConversationStale
} = input;
const startConversation = useCallback(
(
initialContentInput?: unknown,
displayPrompt?: string,
submitOptions?: AgentComposerSubmitOptions,
initialTurnExpected?: boolean,
initialGoalControl?: AgentActivityInitialGoalControl
): AgentGUINewConversationActivationResult | null => {
const target = selectedAgentTargetRef.current;
const targetData = selectedComposerTargetDataRef.current;
if (target.disabled === true) {
return null;
}
const agentTargetId = targetData.agentTargetId ?? "";
if (
!agentTargetId ||
(agentTargetsProvidedRef.current &&
!selectedAgentTargetIsExplicitRef.current)
) {
setDetailError(translate("agentHost.agentGui.agentTargetRequired"));
return null;
}
const normalizedInitialContent = Array.isArray(initialContentInput)
? normalizeAgentPromptContentBlocks(
initialContentInput as AgentPromptContentBlock[]
)
: textPromptContent(normalizeOptionalPrompt(initialContentInput));
const initialDisplayPrompt =
displayPrompt && displayPrompt.trim() ? displayPrompt : undefined;
// bundle 折叠时,标题/回显用 displayPrompt(单 chip),而非展开后的文件列表。
const normalizedInitialPrompt =
initialDisplayPrompt ??
agentPromptContentDisplayText(normalizedInitialContent);
isCreatingConversationRef.current = true;
setDetailError(null);
const provider = targetData.provider;
onDataChangeRef.current((current) =>
current.provider === provider &&
(current.agentTargetId ?? null) === agentTargetId
? current
: {
...current,
provider,
agentTargetId
}
);
const selectedProjectPath = selectedProjectPathRef.current;
const railPlacement = resolveInitialRailPlacement({
selectedProjectPath,
userProjects: userProjectsRef.current
});
if (railPlacement === null) {
return null;
}
const railSectionKey = railPlacement.sectionKey;
const initialNodeSettings = readNodeDefaultDraftSettings({
data: targetData.data,
defaultReasoningEffort,
drafts: draftSettingsBySessionIdRef.current
});
const snapshotComposerOptions = getCachedComposerOptions();
const requiredSettingsPatch = submitOptions?.requiredSettingsPatch as
| Partial<AgentSessionComposerSettings>
| undefined;
// Sparse Create settings must match the home presentation. Draft fields
// retired after remembered-default acknowledgement still appear in
// effectiveSettings / permissionConfig.defaultValue; resolve them here
// so the first turn does not fall back to the provider default.
const settings = resolveSparseNewConversationActivationSettings({
draftSettings: initialNodeSettings,
composerOptions: snapshotComposerOptions,
requiredSettingsPatch,
codexSaverModeEntryEnabled,
rtkSaverModeEntryEnabled
});
const settingProvenance = resolveNewConversationSettingProvenance(
requiredSettingsPatch
);
const prewarmedSessionId =
normalizedInitialContent.length > 0 &&
snapshotComposerOptions?.behavior?.prewarmDraftSession === true
? draftAgentSessionIdFromComposerOptions(snapshotComposerOptions)
: null;
const agentSessionId =
prewarmedSessionId &&
activation.stateFor(prewarmedSessionId) === "inactive" &&
isPendingActivationViable(
selectLatestActivationForSession(
sessionEngine.getSnapshot(),
prewarmedSessionId
)
)
? prewarmedSessionId
: createAgentGUIConversationId();
const submitTrace = createAgentSubmitTraceState({
agentSessionId,
content: normalizedInitialContent,
prompt: normalizedInitialPrompt,
queued: false,
startedAtUnixMs: Date.now()
});
const sourceScopeKey = resolveAgentComposerDraftScopeKey({});
const submittedDraft =
submitOptions?.submittedDraft ??
draftByScopeKeyRef.current[sourceScopeKey] ??
emptyAgentComposerDraft();
submittedDraftSnapshotsRef.current[submitTrace.clientSubmitId] = {
sourceScopeKey,
content: snapshotAgentComposerDraft(submittedDraft)
};
const engineSnapshot = sessionEngine.getSnapshot();
const draftPreferences = selectTuttiModeDraftPreferences(
engineSnapshot,
tuttiModeDraftKey
);
const initialTuttiMode = resolveInitialTuttiModeActivation({
submitOptions,
draftActive: selectTuttiModeDraftIsActive(
engineSnapshot,
tuttiModeDraftKey
),
draftEffect: draftPreferences.effect,
draftSpeed: draftPreferences.speed
});
reportAgentSubmitTraceDiagnostic({
event: "activation.requested",
runtime: agentActivityRuntime,
trace: submitTrace,
workspaceId,
fields: {
mode: "new",
tutti_mode_active: initialTuttiMode !== null,
tutti_mode_source: initialTuttiMode?.source ?? "inactive"
}
});
const requestId = activation.activate({
mode: "new",
agentSessionId,
agentTargetId,
...(submitOptions?.isolation
? { isolation: submitOptions.isolation }
: {}),
...settingProvenance,
...(submitOptions?.capabilityRefs?.length
? { capabilityRefs: submitOptions.capabilityRefs }
: {}),
clientSubmitId: submitTrace.clientSubmitId,
cwd: selectedProjectPath ?? "",
railPlacement,
railSectionKey,
initialContent: normalizedInitialContent,
...(initialTurnExpected !== undefined ? { initialTurnExpected } : {}),
...(initialGoalControl ? { initialGoalControl } : {}),
initialDisplayPrompt,
...(initialTuttiMode
? {
initialTuttiModeActivation: initialTuttiMode.activation,
tuttiModeDraftKey
}
: {}),
runtimeContent: toRuntimeSendContent(normalizedInitialContent),
submitDiagnostics: agentSubmitTraceDiagnostics(submitTrace),
settings,
optimisticTitle: deriveAgentGUIOptimisticConversationTitle(
normalizedInitialPrompt
)
});
if (requestId === null) return null;
activeConversationIdRef.current = agentSessionId;
setActiveConversationId(agentSessionId);
requestRailReveal(agentSessionId, "created");
isComposerHomeRef.current = false;
setIsComposerHome(false);
setIntent({
id: agentSessionId,
source: "activation",
tag: "active"
});
setIsLoadingMessages(false);
return { agentSessionId, requestId };
},
[
activeSessionState,
codexSaverModeEntryEnabled,
rtkSaverModeEntryEnabled,
currentUserId,
data,
defaultReasoningEffort,
getCachedComposerOptions,
requestRailReveal,
activation,
conversationListQuery,
isCurrentConversation,
agentActivityRuntime,
isConversationStale,
sessionEngine,
tuttiModeDraftKey,
workspaceId
]
);
return startConversation;
}