源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
+414
@@ -0,0 +1,414 @@
|
||||
// Agent GUI controller — pure composer target and settings presentation policy.
|
||||
|
||||
import type { AgentActivityComposerOptions } from "@tutti-os/agent-activity-core";
|
||||
import type {
|
||||
AgentSessionComposerSettings,
|
||||
AgentSessionReasoningEffort,
|
||||
AgentSessionSpeed
|
||||
} from "../../../shared/agentSessionTypes";
|
||||
import type { AgentGUINodeData, AgentGUIProvider } from "../../../types";
|
||||
import type { ACPConfigOptionSelection } from "./agentGuiController.types";
|
||||
import { reasoningSelectionForModelFromComposerOptions } from "./agentGuiController.composerHelpers";
|
||||
import { normalizeOptionalText } from "./agentGuiController.promptHelpers";
|
||||
|
||||
export interface AgentGUIComposerTargetData {
|
||||
agentTargetId: string | null;
|
||||
data: AgentGUINodeData;
|
||||
provider: AgentGUIProvider;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export interface AgentGUIActiveSessionTarget {
|
||||
agentTargetId: string | null;
|
||||
agentSessionId: string;
|
||||
provider: AgentGUIProvider;
|
||||
}
|
||||
|
||||
export interface OptimisticComposerTarget {
|
||||
agentSessionId: string;
|
||||
target: AgentGUIComposerTargetData;
|
||||
}
|
||||
|
||||
export function isConversationCreateActive(input: {
|
||||
activeConversationId: string | null;
|
||||
pendingConversationId: string | null;
|
||||
}): boolean {
|
||||
return (
|
||||
input.pendingConversationId !== null &&
|
||||
(input.activeConversationId === null ||
|
||||
input.activeConversationId === input.pendingConversationId)
|
||||
);
|
||||
}
|
||||
|
||||
export function composerTargetDataFromNodeData(
|
||||
data: AgentGUINodeData
|
||||
): AgentGUIComposerTargetData {
|
||||
const agentTargetId = normalizeOptionalText(data.agentTargetId);
|
||||
return {
|
||||
agentTargetId,
|
||||
provider: data.provider,
|
||||
targetId: agentTargetId ?? `local:${data.provider}`,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the device-local model-history owner without guessing an active
|
||||
* Session's identity. Home may use its stable target fallback, while an active
|
||||
* Session without a canonical Agent Target fails closed.
|
||||
*/
|
||||
export function composerModelChoiceHistoryTargetId(input: {
|
||||
activeConversationId: string | null;
|
||||
target: Pick<AgentGUIComposerTargetData, "agentTargetId" | "targetId">;
|
||||
}): string | null {
|
||||
const agentTargetId = normalizeOptionalText(input.target.agentTargetId);
|
||||
if (agentTargetId) {
|
||||
return agentTargetId;
|
||||
}
|
||||
return input.activeConversationId === null
|
||||
? normalizeOptionalText(input.target.targetId)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function composerTargetDataForConversation(input: {
|
||||
activeConversationId: string | null;
|
||||
activeSessionTarget: AgentGUIActiveSessionTarget | null;
|
||||
data: AgentGUINodeData;
|
||||
optimisticTarget: OptimisticComposerTarget | null;
|
||||
selectedTarget: AgentGUIComposerTargetData;
|
||||
}): AgentGUIComposerTargetData {
|
||||
if (input.activeConversationId === null) {
|
||||
return input.selectedTarget;
|
||||
}
|
||||
if (
|
||||
input.optimisticTarget?.agentSessionId === input.activeConversationId &&
|
||||
(!input.activeSessionTarget ||
|
||||
input.activeSessionTarget.agentSessionId !== input.activeConversationId ||
|
||||
!nodeDataMatchesComposerTarget(input.data, input.optimisticTarget.target))
|
||||
) {
|
||||
return input.optimisticTarget.target;
|
||||
}
|
||||
if (
|
||||
!input.activeSessionTarget ||
|
||||
input.activeSessionTarget.agentSessionId !== input.activeConversationId
|
||||
) {
|
||||
return {
|
||||
agentTargetId: null,
|
||||
provider: input.data.provider,
|
||||
targetId: "__active_session_loading__",
|
||||
data: {
|
||||
...input.data,
|
||||
agentTargetId: null
|
||||
}
|
||||
};
|
||||
}
|
||||
const activeAgentTargetId = normalizeOptionalText(
|
||||
input.activeSessionTarget.agentTargetId
|
||||
);
|
||||
const dataMatchesActiveSessionTarget =
|
||||
input.data.provider === input.activeSessionTarget.provider &&
|
||||
normalizeOptionalText(input.data.agentTargetId) === activeAgentTargetId;
|
||||
return {
|
||||
agentTargetId: activeAgentTargetId,
|
||||
provider: input.activeSessionTarget.provider,
|
||||
targetId:
|
||||
activeAgentTargetId ?? `local:${input.activeSessionTarget.provider}`,
|
||||
data: dataMatchesActiveSessionTarget
|
||||
? input.data
|
||||
: {
|
||||
...input.data,
|
||||
agentTargetId: activeAgentTargetId,
|
||||
provider: input.activeSessionTarget.provider
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function reconcileOptimisticComposerTarget(input: {
|
||||
activeConversationId: string | null;
|
||||
data: AgentGUINodeData;
|
||||
optimisticTarget: OptimisticComposerTarget | null;
|
||||
}): OptimisticComposerTarget | null {
|
||||
const optimisticTarget = input.optimisticTarget;
|
||||
if (
|
||||
!optimisticTarget ||
|
||||
input.activeConversationId !== optimisticTarget.agentSessionId ||
|
||||
nodeDataMatchesComposerTarget(input.data, optimisticTarget.target)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return optimisticTarget;
|
||||
}
|
||||
|
||||
export function nodeDataMatchesComposerTarget(
|
||||
data: AgentGUINodeData,
|
||||
target: AgentGUIComposerTargetData
|
||||
): boolean {
|
||||
return (
|
||||
data.provider === target.provider &&
|
||||
normalizeOptionalText(data.agentTargetId) === target.agentTargetId
|
||||
);
|
||||
}
|
||||
|
||||
export function isForegroundModelOptionsLoading(input: {
|
||||
modelOptionsLoading: boolean | undefined;
|
||||
selection: ACPConfigOptionSelection | null;
|
||||
supportsModel: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
input.supportsModel &&
|
||||
input.modelOptionsLoading === true &&
|
||||
(input.selection === null || input.selection.options.length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function effectiveComposerSettingsFromOptions(
|
||||
options: AgentActivityComposerOptions | null
|
||||
): AgentSessionComposerSettings | null {
|
||||
const settings = options?.effectiveSettings;
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
model: normalizeOptionalText(settings.model),
|
||||
reasoningEffort: normalizeOptionalText(
|
||||
settings.reasoningEffort
|
||||
) as AgentSessionReasoningEffort | null,
|
||||
speed: normalizeOptionalText(settings.speed) as AgentSessionSpeed | null,
|
||||
planMode: settings.planMode ?? undefined,
|
||||
permissionModeId: normalizeOptionalText(settings.permissionModeId)
|
||||
};
|
||||
}
|
||||
|
||||
function composerOptionValues(
|
||||
options: readonly { value: string }[]
|
||||
): ReadonlySet<string> {
|
||||
return new Set(options.map((option) => option.value));
|
||||
}
|
||||
|
||||
export function sanitizeComposerSettingsForOptions(
|
||||
settings: AgentSessionComposerSettings,
|
||||
options: AgentActivityComposerOptions | null
|
||||
): AgentSessionComposerSettings {
|
||||
if (!options) {
|
||||
return settings;
|
||||
}
|
||||
const modelValues = composerOptionValues(options.models);
|
||||
const reasoningValues = composerOptionValues(options.reasoningEfforts);
|
||||
const speedValues = composerOptionValues(options.speeds ?? []);
|
||||
const permissionValues = new Set(
|
||||
options.permissionConfig?.modes.map((mode) => mode.id) ?? []
|
||||
);
|
||||
const model = normalizeOptionalText(settings.model);
|
||||
const reasoningEffort = normalizeOptionalText(settings.reasoningEffort);
|
||||
const speed = normalizeOptionalText(settings.speed);
|
||||
const permissionModeId = normalizeOptionalText(settings.permissionModeId);
|
||||
const modelReasoningSelection = reasoningSelectionForModelFromComposerOptions(
|
||||
options,
|
||||
reasoningEffort as AgentSessionReasoningEffort | null,
|
||||
model
|
||||
);
|
||||
return {
|
||||
...settings,
|
||||
model:
|
||||
options.behavior?.modelOptionsAuthoritative === true &&
|
||||
model &&
|
||||
modelValues.size > 0 &&
|
||||
!modelValues.has(model)
|
||||
? null
|
||||
: model,
|
||||
reasoningEffort:
|
||||
modelReasoningSelection !== null
|
||||
? modelReasoningSelection.currentValue
|
||||
: options.reasoningConfigurable !== true
|
||||
? null
|
||||
: reasoningEffort &&
|
||||
reasoningValues.size > 0 &&
|
||||
!reasoningValues.has(reasoningEffort)
|
||||
? null
|
||||
: (reasoningEffort as AgentSessionReasoningEffort | null),
|
||||
speed:
|
||||
speed && speedValues.size > 0 && !speedValues.has(speed)
|
||||
? null
|
||||
: (speed as AgentSessionSpeed | null),
|
||||
permissionModeId:
|
||||
permissionModeId &&
|
||||
permissionValues.size > 0 &&
|
||||
!permissionValues.has(permissionModeId)
|
||||
? null
|
||||
: permissionModeId
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeComposerSettingsForTarget(input: {
|
||||
settings: AgentSessionComposerSettings;
|
||||
target: AgentGUIComposerTargetData;
|
||||
options: AgentActivityComposerOptions | null;
|
||||
}): AgentSessionComposerSettings {
|
||||
if (!input.target.agentTargetId) {
|
||||
return input.settings;
|
||||
}
|
||||
return sanitizeComposerSettingsForOptions(input.settings, input.options);
|
||||
}
|
||||
|
||||
export type ComposerNativeModelVerdict =
|
||||
| "verified"
|
||||
| "rejected"
|
||||
| "unverifiable";
|
||||
|
||||
export interface ComposerNativeModelOptionsTestimony {
|
||||
models: readonly {
|
||||
value: string;
|
||||
requested?: boolean;
|
||||
}[];
|
||||
modelOptionsLoading?: boolean;
|
||||
effectiveSettings?: {
|
||||
model?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verdict of a bare model id against the provider-native options list. Only
|
||||
* settled catalog entries are testimony:
|
||||
* - options missing or the model catalog still loading have no opinion
|
||||
* ("unverifiable");
|
||||
* - requested-origin entries (daemon warm-catalog append of the requested
|
||||
* model, selected-model bootstrap echo, GUI current-value append) mirror
|
||||
* the request rather than the catalog and are excluded — a list with no
|
||||
* catalog entries left proves nothing;
|
||||
* - as a fallback for options produced before provenance marking, a
|
||||
* single-entry list mirroring the effective selection is recognized as the
|
||||
* daemon's selected-model bootstrap echo (composerSelectedModelOptions)
|
||||
* and proves nothing either;
|
||||
* - otherwise the catalog either contains the model ("verified") or
|
||||
* positively rejects it ("rejected").
|
||||
*/
|
||||
export function verifyComposerModelAgainstNativeOptions(
|
||||
model: string,
|
||||
options: ComposerNativeModelOptionsTestimony | null
|
||||
): ComposerNativeModelVerdict {
|
||||
if (options === null || options.modelOptionsLoading === true) {
|
||||
return "unverifiable";
|
||||
}
|
||||
const catalogEntries = options.models.filter(
|
||||
(option) => option.requested !== true
|
||||
);
|
||||
if (catalogEntries.length === 0) {
|
||||
return "unverifiable";
|
||||
}
|
||||
const isSelectedModelEcho =
|
||||
catalogEntries.length === 1 &&
|
||||
catalogEntries[0]!.value === model &&
|
||||
normalizeOptionalText(options.effectiveSettings?.model) === model;
|
||||
if (isSelectedModelEcho) {
|
||||
return "unverifiable";
|
||||
}
|
||||
return catalogEntries.some((option) => option.value === model)
|
||||
? "verified"
|
||||
: "rejected";
|
||||
}
|
||||
|
||||
/**
|
||||
* Home-composer default policy: a pure provider target must never adopt a
|
||||
* bare model that the settled provider-native list rejects — it falls back
|
||||
* to the provider default instead of presenting (and later submitting) a
|
||||
* model the provider cannot run. This only acts on a positive rejection:
|
||||
* while options are missing or the catalog is loading the stored default is
|
||||
* left alone so a transient load state cannot destroy a legitimate
|
||||
* remembered model.
|
||||
*/
|
||||
export function enforceComposerModelBindingForHomeDefaults(
|
||||
settings: AgentSessionComposerSettings,
|
||||
options: AgentActivityComposerOptions | null
|
||||
): AgentSessionComposerSettings {
|
||||
const model = normalizeOptionalText(settings.model);
|
||||
const modelPlanId = normalizeOptionalText(settings.modelPlanId);
|
||||
if (!model || modelPlanId) {
|
||||
return settings;
|
||||
}
|
||||
return verifyComposerModelAgainstNativeOptions(model, options) === "rejected"
|
||||
? { ...settings, model: null, modelPlanId: null }
|
||||
: settings;
|
||||
}
|
||||
|
||||
export function resolvePresentedComposerSettings(input: {
|
||||
homeSettings: AgentSessionComposerSettings;
|
||||
optimisticSettings: AgentSessionComposerSettings | null;
|
||||
preloadedSettings: AgentSessionComposerSettings | null;
|
||||
sessionSettings: AgentSessionComposerSettings | null;
|
||||
}): AgentSessionComposerSettings {
|
||||
const layers = [
|
||||
input.sessionSettings,
|
||||
input.optimisticSettings,
|
||||
input.preloadedSettings,
|
||||
input.homeSettings
|
||||
];
|
||||
const firstText = (
|
||||
field: "model" | "reasoningEffort" | "speed" | "permissionModeId"
|
||||
): string | null => {
|
||||
for (const layer of layers) {
|
||||
const value = normalizeOptionalText(layer?.[field]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const firstBoolean = (
|
||||
field:
|
||||
| "codexSaverMode"
|
||||
| "rtkSaverMode"
|
||||
| "planMode"
|
||||
| "browserUse"
|
||||
| "computerUse",
|
||||
fallback: boolean
|
||||
): boolean => {
|
||||
for (const layer of layers) {
|
||||
const value = layer?.[field];
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
return {
|
||||
codexSaverMode: firstBoolean("codexSaverMode", false),
|
||||
rtkSaverMode: firstBoolean("rtkSaverMode", false),
|
||||
model: firstText("model"),
|
||||
reasoningEffort: firstText(
|
||||
"reasoningEffort"
|
||||
) as AgentSessionReasoningEffort | null,
|
||||
speed: firstText("speed") as AgentSessionSpeed | null,
|
||||
planMode: firstBoolean("planMode", false),
|
||||
browserUse: firstBoolean("browserUse", true),
|
||||
computerUse: firstBoolean("computerUse", true),
|
||||
permissionModeId: firstText("permissionModeId")
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveComposerSettingsPresentation(input: {
|
||||
active: boolean;
|
||||
homeSettings: AgentSessionComposerSettings;
|
||||
optimisticSettings?: AgentSessionComposerSettings | null;
|
||||
options: AgentActivityComposerOptions | null;
|
||||
permissionModeId?: string | null;
|
||||
sessionSettings?: AgentSessionComposerSettings | null;
|
||||
}): AgentSessionComposerSettings {
|
||||
const sessionSettings =
|
||||
input.active &&
|
||||
(input.sessionSettings != null || Boolean(input.permissionModeId))
|
||||
? {
|
||||
...(input.sessionSettings ?? {}),
|
||||
permissionModeId:
|
||||
normalizeOptionalText(input.permissionModeId) ??
|
||||
normalizeOptionalText(input.sessionSettings?.permissionModeId)
|
||||
}
|
||||
: null;
|
||||
return resolvePresentedComposerSettings({
|
||||
sessionSettings,
|
||||
optimisticSettings: input.active
|
||||
? (input.optimisticSettings ?? null)
|
||||
: input.homeSettings,
|
||||
preloadedSettings: effectiveComposerSettingsFromOptions(input.options),
|
||||
homeSettings: input.homeSettings
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user