Files
tutti/packages/agent/gui/AgentGUIQuickComposer.tsx
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

352 lines
12 KiB
TypeScript

import { useMemo, type JSX } from "react";
import type {
AgentActivityComposerOptions,
AgentPromptContentBlock
} from "@tutti-os/agent-activity-core";
import type { I18nRuntime } from "@tutti-os/ui-i18n-runtime";
import { TooltipProvider } from "@tutti-os/ui-system";
import type { RichTextMentionService } from "@tutti-os/ui-rich-text/service";
import type { WorkspaceUserProjectApi } from "@tutti-os/workspace-user-project/contracts";
import { createWorkspaceUserProjectI18nRuntime } from "@tutti-os/workspace-user-project/i18n";
import { AgentComposer } from "./agent-gui/agentGuiNode/AgentComposer";
import { AgentGUIMentionServiceBoundary } from "./agent-gui/agentGuiNode/AgentGUIMentionServiceBoundary";
import { useAgentGUIViewLabels } from "./agent-gui/agentGuiNode/AgentGUINode.labels";
import type { AgentComposerProps } from "./agent-gui/agentGuiNode/composer/AgentComposer.types";
import type {
AgentGUIComposerGate,
AgentGUIComposerSettingsVM
} from "./agent-gui/agentGuiNode/model/agentGuiNodeTypes";
import {
agentComposerDraftPrompt,
agentComposerDraftToPromptContent,
agentPromptContentToComposerDraft
} from "./agent-gui/agentGuiNode/model/agentComposerDraft";
import {
AgentGuiI18nProvider,
type AgentGuiI18nLocale,
useTranslation
} from "./i18n/index";
import type { AgentGUIAgentTarget } from "./types";
import {
pickQuickComposerSettings,
projectQuickComposerSettings,
type AgentGUIQuickComposerSettings
} from "./quickComposerSettings";
export type { AgentGUIQuickComposerSettings } from "./quickComposerSettings";
const readyGate: AgentGUIComposerGate = {
conversationBusy: false,
isAwaitingTurnStart: false,
editor: { reason: null, status: "editable" },
runtime: {
reason: null,
sessionRuntimeReason: null,
status: "ready"
},
submission: { reason: null, status: "ready" }
};
const emptyQuickComposerSettings: AgentGUIQuickComposerSettings = Object.freeze(
{}
);
export type AgentGUIQuickComposerAgentTarget = Omit<
AgentGUIAgentTarget,
"agentTargetId" | "ref" | "targetId"
> & {
agentTargetId: string;
};
export interface AgentGUIQuickComposerTargetCapabilities {
imageInput: boolean;
workspaceReferences: boolean;
}
export interface AgentGUIQuickComposerSettingsCapability {
/** Host-owned authoritative options for the exact selected target/cwd. */
options: AgentActivityComposerOptions | null;
loading: boolean;
/** Controlled sparse pre-session settings selected in this draft. */
value: AgentGUIQuickComposerSettings;
onChange(patch: AgentGUIQuickComposerSettings): void;
}
export interface AgentGUIQuickComposerSubmission {
agentTargetId: string;
content: AgentPromptContentBlock[];
displayPrompt?: string;
settings?: AgentGUIQuickComposerSettings;
}
export interface AgentGUIQuickComposerProps {
agentTargets: readonly AgentGUIQuickComposerAgentTarget[];
capabilitiesByAgentTargetId: Readonly<
Record<string, AgentGUIQuickComposerTargetCapabilities | undefined>
>;
composerActionAccessory?: AgentComposerProps["composerActionAccessory"];
composerActionPlacement?: AgentComposerProps["composerActionPlacement"];
/** Omit the whole capability when the host cannot persist settings changes. */
composerSettings?: AgentGUIQuickComposerSettingsCapability;
content: readonly AgentPromptContentBlock[];
disabled?: boolean;
/** Fill a height explicitly assigned by the embedding host. */
fillAvailableHeight?: boolean;
/** Visual treatment for the canonical Composer input surface. */
inputSurfaceVariant?: "default" | "borderless";
/** Host i18n runtime, matching the full AgentGUI embedding contract. */
i18n?: I18nRuntime<string> | null;
locale?: AgentGuiI18nLocale;
/** Mention providers supplied by the embedding host. */
mentionService?: RichTextMentionService;
/** Reserved host chrome above portaled composer menus. */
menuViewportTopInset?: number;
onAgentTargetChange(agentTargetId: string): void;
onContentChange(content: AgentPromptContentBlock[]): void;
onProjectPathChange?: AgentComposerProps["onProjectPathChange"];
onRequestWorkspaceReferences?: AgentComposerProps["onRequestWorkspaceReferences"];
onSubmit(submission: AgentGUIQuickComposerSubmission): void;
placeholder?: string;
selectedProjectPath?: string | null;
selectedAgentTargetId: string;
selectProjectDirectory?: AgentComposerProps["selectProjectDirectory"];
/** Real project catalog and registration capability supplied by the host. */
userProjectApi?: WorkspaceUserProjectApi | null;
workspaceId: string;
}
/**
* A lifecycle-free AgentGUI entry surface for launchers that already have a
* workspace Engine owner. It owns only draft presentation; the Host receives
* the submitted prompt and remains responsible for Session activation.
*/
export function AgentGUIQuickComposer(
props: AgentGUIQuickComposerProps
): JSX.Element {
return (
<AgentGuiI18nProvider locale={props.locale} runtime={props.i18n}>
<TooltipProvider delayDuration={120} skipDelayDuration={0}>
<AgentGUIMentionServiceBoundary service={props.mentionService}>
<AgentGUIQuickComposerInner {...props} />
</AgentGUIMentionServiceBoundary>
</TooltipProvider>
</AgentGuiI18nProvider>
);
}
function AgentGUIQuickComposerInner({
agentTargets,
capabilitiesByAgentTargetId,
composerActionAccessory,
composerActionPlacement,
composerSettings: settingsCapability,
content,
disabled = false,
fillAvailableHeight = false,
inputSurfaceVariant = "default",
menuViewportTopInset,
onAgentTargetChange,
onContentChange,
onProjectPathChange,
onRequestWorkspaceReferences,
onSubmit,
placeholder,
selectedAgentTargetId,
selectedProjectPath = null,
selectProjectDirectory,
userProjectApi,
workspaceId
}: AgentGUIQuickComposerProps): JSX.Element {
const { i18n, locale, t } = useTranslation();
const selectedAgentTarget =
agentTargets.find(
(target) => target.agentTargetId === selectedAgentTargetId
) ?? null;
const resolvedAgentTargetId = selectedAgentTarget?.agentTargetId ?? null;
const selectedTargetCapabilities = selectedAgentTarget
? (capabilitiesByAgentTargetId[selectedAgentTarget.agentTargetId] ?? null)
: null;
const selectedTargetReady =
selectedAgentTarget !== null &&
selectedAgentTarget.disabled !== true &&
selectedTargetCapabilities !== null;
const draftHasImages = content.some((block) => block.type === "image");
const provider = selectedAgentTarget?.provider ?? "";
const composerOptions = settingsCapability?.options ?? null;
const composerOptionsLoading = settingsCapability?.loading ?? false;
const settingsCapabilityValue = settingsCapability?.value;
const settings = useMemo(
() =>
settingsCapabilityValue
? pickQuickComposerSettings(settingsCapabilityValue)
: emptyQuickComposerSettings,
[settingsCapabilityValue]
);
const composerAgentTargets = useMemo<AgentGUIAgentTarget[]>(
() =>
agentTargets.map((target) => ({
...target,
agentTargetId: target.agentTargetId,
ref: { kind: "quick-composer", provider: target.provider },
targetId: target.agentTargetId
})),
[agentTargets]
);
const selectedComposerAgentTarget =
composerAgentTargets.find(
(target) => target.agentTargetId === selectedAgentTargetId
) ?? null;
const labels = useAgentGUIViewLabels({
displayProviderLabel: selectedAgentTarget?.label ?? provider,
fallbackAgentTitle: selectedAgentTarget?.label ?? provider,
t,
workspaceAppIcons: [],
workspaceId
});
const workspaceUserProjectI18n = useMemo(
() => createWorkspaceUserProjectI18nRuntime(i18n),
[i18n]
);
const draftContent = useMemo(
() => agentPromptContentToComposerDraft(content, "quick-composer"),
[content]
);
const composerSettings = useMemo<AgentGUIComposerSettingsVM>(
() =>
projectQuickComposerSettings({
agentTargetId: resolvedAgentTargetId,
loading: composerOptionsLoading,
options: composerOptions,
projectLocked: disabled,
provider,
selectedProjectPath,
settings
}),
[
composerOptions,
composerOptionsLoading,
disabled,
provider,
resolvedAgentTargetId,
selectedProjectPath,
settings
]
);
return (
<div
className="agent-gui-node__shell"
data-quick-composer-input-surface={inputSurfaceVariant}
>
<AgentComposer
activePrompt={null}
agentTargets={composerAgentTargets}
availableCommands={[]}
// Quick Composer hosts expose no capability-settings channel, so the
// connector control could only render as a dead trigger. Hiding it
// also keeps the footer controls on one alignment baseline.
capabilityMenuState={{ connectors: { enabled: false } }}
canGoalControl={false}
canUploadAttachment={Boolean(
selectedTargetCapabilities?.imageInput ||
selectedTargetCapabilities?.workspaceReferences
)}
composerActionAccessory={composerActionAccessory}
composerActionPlacement={composerActionPlacement}
composerSettings={composerSettings}
drainingQueuedPromptId={null}
draftContent={draftContent}
gate={readyGate}
fillAvailableHeight={fillAvailableHeight}
isInterrupting={false}
isSendingTurn={false}
isSubmittingPrompt={disabled}
layoutMode="embedded"
menuViewportTopInset={menuViewportTopInset}
labels={{
...labels,
approvalLead: labels.approvalRequired,
fileChangeApprovalLead: labels.fileChangeApprovalRequired
}}
placeholder={placeholder ?? labels.initialPlaceholder}
presentationEditorDisabled={disabled}
presentationSubmitDisabled={
disabled ||
composerOptionsLoading ||
!selectedTargetReady ||
(draftHasImages && selectedTargetCapabilities?.imageInput !== true)
}
projectMissingProbeEnabled={false}
promptImagesSupported={selectedTargetCapabilities?.imageInput === true}
provider={provider}
providerSelectLabel={labels.providerSwitchLabel}
queuedPrompts={[]}
selectedAgentTarget={selectedComposerAgentTarget}
selectProjectDirectory={selectProjectDirectory}
showProjectSelectorInFooter={Boolean(
userProjectApi && onProjectPathChange
)}
showStopButton={false}
stopDisabled={true}
uiLanguage={locale}
workspaceId={workspaceId}
workspaceUserProjectI18n={workspaceUserProjectI18n}
userProjectApi={userProjectApi}
onDraftContentChange={(nextDraft) => {
onContentChange(
agentComposerDraftToPromptContent({
draft: nextDraft,
skills: []
})
);
}}
onEditQueuedPrompt={() => {}}
onInterruptCurrentTurn={() => {}}
onProviderSelect={({ agentTargetId }) => {
if (
agentTargetId &&
agentTargets.some(
(target) =>
target.agentTargetId === agentTargetId &&
target.disabled !== true
)
) {
onAgentTargetChange(agentTargetId);
}
}}
onProjectPathChange={onProjectPathChange}
onRemoveQueuedPrompt={() => {}}
onRequestWorkspaceReferences={
selectedTargetCapabilities?.workspaceReferences === true
? onRequestWorkspaceReferences
: undefined
}
onSendQueuedPromptNext={() => {}}
onSettingsChange={(patch) =>
settingsCapability?.onChange(pickQuickComposerSettings(patch))
}
onSubmit={(nextContent, displayPrompt) => {
if (
!selectedTargetReady ||
!selectedAgentTarget ||
!selectedTargetCapabilities ||
(nextContent.some((block) => block.type === "image") &&
selectedTargetCapabilities.imageInput !== true)
) {
return;
}
onSubmit({
agentTargetId: selectedAgentTarget.agentTargetId,
content: nextContent,
displayPrompt:
displayPrompt ?? agentComposerDraftPrompt(draftContent),
...(settingsCapability && Object.keys(settings).length > 0
? { settings }
: {})
});
}}
onSubmitInteractivePrompt={() => false}
/>
</div>
);
}