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

453 lines
14 KiB
TypeScript

import { useRef } from "react";
import { normalizeOptionalWorkspaceAgentStatus } from "../../../shared/workspaceAgentStatusNormalizer";
import type { UiLanguage } from "../../../contexts/settings/domain/agentSettings";
import type { AgentMessageMarkdownWorkspaceAppIcon } from "../../../shared/AgentMessageMarkdown";
import type {
AgentConversationPromptVM,
AgentConversationVM
} from "../../../shared/agentConversation/contracts/agentConversationVM";
import { createAgentSessionHandoffPrompt } from "../agentRichText/agentFileMentionExtension";
import type {
AgentComposerSlashStatus,
AgentComposerSlashStatusLimit
} from "../AgentComposer";
import type {
AgentGUINodeViewModel,
AgentGUISessionChrome
} from "../model/agentGuiNodeTypes";
import type { AgentGUIAgentTarget } from "../../../types";
import type { AgentGUIViewLabels } from "./AgentGUINodeView.types";
import { conversationPlainTitle, stringValue } from "./agentGUIViewUtils";
export { isDifferentKnownConversationOwner } from "../model/agentGuiComposerGate";
export function resolveAgentGUIConversationReturn(
labels: Pick<
AgentGUIViewLabels,
"continueAnswering" | "returnToConversation"
>,
enabled = true
): { continueAnswering: string; returnToConversation: string } | undefined {
return enabled && labels.continueAnswering && labels.returnToConversation
? {
continueAnswering: labels.continueAnswering,
returnToConversation: labels.returnToConversation
}
: undefined;
}
export function commandAppSource(
command: unknown
): Record<string, unknown> | null {
if (!command || typeof command !== "object" || !("source" in command)) {
return null;
}
const source = (command as { source?: unknown }).source;
if (!source || typeof source !== "object") return null;
const sourceRecord = source as Record<string, unknown>;
return sourceRecord.kind === "app" ? sourceRecord : null;
}
export function workspaceAppIconKey(
appId: string,
workspaceId: string
): string {
return `${workspaceId}\u0000${appId}`;
}
export function isContextCanceledMessage(
message: string | null | undefined
): boolean {
const normalized = message?.trim().toLowerCase() ?? "";
return normalized === "context canceled";
}
export function resolveConversationDetailStatus(
detail: AgentGUINodeViewModel["detail"]["conversationDetail"]
): AgentGUINodeViewModel["rail"]["conversations"][number]["status"] | null {
if (!detail) {
return null;
}
const normalized = normalizeOptionalWorkspaceAgentStatus({
activeTurnPhase: detail.session.activeTurn?.phase,
latestTurnOutcome: detail.session.latestTurn?.outcome
});
switch (normalized?.kind) {
case "working":
return "working";
case "waiting":
return "waiting";
case "failed":
return "failed";
case "completed":
return "completed";
case "canceled":
return "canceled";
case "ready":
return "ready";
default:
return null;
}
}
export function resolveSlashStatus({
rawState,
limits,
limitsLoading,
limitsUnavailable,
usage
}: {
rawState: AgentGUISessionChrome["rawState"];
limits: readonly AgentComposerSlashStatusLimit[];
limitsLoading: boolean;
limitsUnavailable: boolean;
usage: AgentGUINodeViewModel["detail"]["usage"];
}): AgentComposerSlashStatus {
const usedTokens = usage?.usedTokens ?? null;
const totalTokens = usage?.totalTokens ?? null;
return {
agentSessionId: rawState?.agentSessionId ?? null,
baseUrl: null,
limits,
limitsLoading,
limitsUnavailable,
contextWindow:
usedTokens !== null && totalTokens !== null
? { usedTokens, totalTokens }
: null
};
}
function slashStatusLimitsEqual(
left: readonly AgentComposerSlashStatusLimit[] | null | undefined,
right: readonly AgentComposerSlashStatusLimit[] | null | undefined
): boolean {
const leftLimits = left ?? [];
const rightLimits = right ?? [];
return (
leftLimits.length === rightLimits.length &&
leftLimits.every((limit, index) => {
const rightLimit = rightLimits[index]!;
return (
limit.id === rightLimit.id &&
limit.label === rightLimit.label &&
(limit.percentRemaining ?? null) ===
(rightLimit.percentRemaining ?? null) &&
limit.value === rightLimit.value
);
})
);
}
function slashStatusesEqual(
left: AgentComposerSlashStatus,
right: AgentComposerSlashStatus
): boolean {
return (
(left.agentSessionId ?? null) === (right.agentSessionId ?? null) &&
(left.baseUrl ?? null) === (right.baseUrl ?? null) &&
(left.contextWindow?.usedTokens ?? null) ===
(right.contextWindow?.usedTokens ?? null) &&
(left.contextWindow?.totalTokens ?? null) ===
(right.contextWindow?.totalTokens ?? null) &&
slashStatusLimitsEqual(left.limits, right.limits) &&
Boolean(left.limitsLoading) === Boolean(right.limitsLoading) &&
Boolean(left.limitsUnavailable) === Boolean(right.limitsUnavailable)
);
}
export function useStableSlashStatus(
status: AgentComposerSlashStatus
): AgentComposerSlashStatus {
const statusRef = useRef<AgentComposerSlashStatus | null>(null);
if (
statusRef.current === null ||
!slashStatusesEqual(statusRef.current, status)
) {
statusRef.current = status;
}
return statusRef.current;
}
function conversationHasActiveWork(
conversation: AgentConversationVM | null | undefined
): boolean {
return (
conversation?.rows.some((row) => {
if (row.kind === "processing") {
return true;
}
if (row.kind === "tool-group") {
return row.calls.some(
(call) =>
call.statusKind === "working" || call.statusKind === "waiting"
);
}
if (row.kind === "message") {
return row.thinking.some(
(thinking) =>
thinking.statusKind === "working" ||
thinking.statusKind === "waiting"
);
}
return false;
}) ?? false
);
}
function isSettledConversationStatus(
status:
| AgentGUINodeViewModel["rail"]["conversations"][number]["status"]
| null
| undefined
): boolean {
return status === "completed" || status === "failed" || status === "canceled";
}
export function resolveActiveConversationBusyStatus(input: {
conversationStatus:
| AgentGUINodeViewModel["rail"]["conversations"][number]["status"]
| undefined;
detailStatus:
| AgentGUINodeViewModel["rail"]["conversations"][number]["status"]
| null;
conversation: AgentConversationVM | null | undefined;
}): AgentGUINodeViewModel["rail"]["conversations"][number]["status"] | null {
if (
input.conversationStatus === "waiting" ||
input.detailStatus === "waiting"
) {
return "waiting";
}
if (
input.conversationStatus === "working" ||
input.detailStatus === "working"
) {
return "working";
}
if (
isSettledConversationStatus(input.conversationStatus) ||
isSettledConversationStatus(input.detailStatus)
) {
return null;
}
if (conversationHasActiveWork(input.conversation)) {
return "working";
}
return null;
}
export function shouldShowAgentGUIStopButton(input: {
hasPendingApproval: boolean;
hasPendingInteractivePrompt: boolean;
isAuthBlocked: boolean;
isCancelPending: boolean;
isConversationBusy: boolean;
isCreatingConversation: boolean;
hasPendingSubmitStopTarget: boolean;
isInterrupting: boolean;
isSubmitting: boolean;
isUnavailable: boolean;
}): boolean {
if (input.isUnavailable || input.isAuthBlocked) return false;
if (input.isCreatingConversation || input.isCancelPending) return true;
if (input.hasPendingSubmitStopTarget) return true;
return (
!input.isSubmitting &&
(input.isConversationBusy ||
input.hasPendingApproval ||
input.hasPendingInteractivePrompt ||
input.isInterrupting)
);
}
export function isAgentGUIHomeStatusNoticeVisible(
recovery: AgentGUISessionChrome["recovery"]
): boolean {
if (
recovery &&
"interactionScoped" in recovery &&
recovery.interactionScoped === true
) {
return false;
}
return (
recovery?.kind === "agent-sharing-revoked" ||
recovery?.kind === "transport-connecting" ||
recovery?.kind === "transport-unavailable"
);
}
export function resolveAgentGUIInteractionDisabledReason(input: {
promptKind: AgentConversationPromptVM["kind"] | null | undefined;
approvalReason: string | null;
interactivePromptReason: string | null;
}): string | null {
if (input.promptKind === null || input.promptKind === undefined) {
return null;
}
return input.promptKind === "approval"
? input.approvalReason
: input.interactivePromptReason;
}
export function resolveAgentGUIComposerInteractionDisabledReason(
promptKind: AgentConversationPromptVM["kind"] | null | undefined,
interaction: AgentGUINodeViewModel["interaction"]
): string | null {
return resolveAgentGUIInteractionDisabledReason({
promptKind,
approvalReason: interaction.approvalDisabledReason,
interactivePromptReason: interaction.interactivePromptDisabledReason
});
}
export function resolveAgentGUIHomeNoticeChrome(input: {
inlineNoticeChrome: AgentGUISessionChrome | null;
sessionChrome: AgentGUISessionChrome;
}): AgentGUISessionChrome | null {
return isAgentGUIHomeStatusNoticeVisible(input.sessionChrome.recovery)
? input.sessionChrome
: input.inlineNoticeChrome;
}
export function resolveAgentGUIStopControl(input: {
hasPendingApproval: boolean;
hasPendingInteractivePrompt: boolean;
isAuthBlocked: boolean;
isCancelPending: boolean;
isConversationBusy: boolean;
isCreatingConversation: boolean;
hasPendingSubmitStopTarget: boolean;
isInterrupting: boolean;
isSubmitting: boolean;
isUnavailable: boolean;
}): { disabled: boolean; visible: boolean } {
return {
// Stop is a daemon/session control, not a composer command. The runtime
// gate may block new submissions while the daemon can still cancel the
// active turn, so do not inherit the composer gate here.
disabled: false,
visible: shouldShowAgentGUIStopButton(input)
};
}
export function resolveAgentGUITuttiStopTargets(input: {
executionActive: boolean;
sourceHasStoppableWork: boolean;
}): { stopExecution: boolean; stopSession: boolean } {
return {
stopExecution: input.executionActive,
stopSession: !input.executionActive || input.sourceHasStoppableWork
};
}
export function buildAgentConversationHandoffPrompt(input: {
activeConversation: AgentGUINodeViewModel["rail"]["activeConversation"];
currentUserId?: string | null;
labels: Pick<AgentGUIViewLabels, "untitledConversationTitle">;
selectedAgentTarget: AgentGUIAgentTarget | null;
uiLanguage: UiLanguage;
workspaceId: string;
}): string {
const conversation = input.activeConversation;
if (!conversation) {
return "";
}
const sourceAgentLabel =
input.selectedAgentTarget?.label?.trim() || conversation.provider;
const title = conversationPlainTitle(
conversation,
input.labels,
input.uiLanguage
);
const mentionLabel = title || sourceAgentLabel;
return createAgentSessionHandoffPrompt({
agentTargetId: conversation.agentTargetId,
agentSessionId: conversation.id,
label: mentionLabel,
workspaceId: input.workspaceId
});
}
export function handoffProjectPathForConversation(
conversation: AgentGUINodeViewModel["rail"]["activeConversation"]
): string | null {
if (conversation?.railSectionKey?.trim() === "conversations") {
return null;
}
return (
conversation?.project?.path?.trim() || conversation?.cwd?.trim() || null
);
}
export function agentGUIDetailBottomDockStoreRevision(input: {
activePromptResponsePending: boolean;
bottomDockLiftedPrompt: { requestId?: string } | null | undefined;
bottomDockReplacementPrompt: { requestId?: string } | null | undefined;
inlineNoticeChrome:
| { recovery?: { message?: string } | null }
| null
| undefined;
sessionChrome: AgentGUISessionChrome;
viewModel: Pick<AgentGUINodeViewModel, "composer">;
}): string {
return [
input.bottomDockLiftedPrompt?.requestId ?? "",
input.bottomDockReplacementPrompt?.requestId ?? "",
input.inlineNoticeChrome?.recovery?.message ?? "",
input.sessionChrome.auth?.message ?? "",
input.sessionChrome.recovery?.kind ?? "",
input.sessionChrome.recovery?.message ?? "",
input.viewModel.composer.queuedPrompts.map((prompt) => prompt.id).join(","),
input.viewModel.composer.queueStatus,
input.viewModel.composer.drainingQueuedPromptId ?? "",
input.activePromptResponsePending ? "1" : "0"
].join("|");
}
export function mergeWorkspaceAppIconsFromCommands(input: {
commands: AgentGUINodeViewModel["composer"]["availableCommands"];
workspaceAppIcons: readonly AgentMessageMarkdownWorkspaceAppIcon[];
workspaceId: string;
}): readonly AgentMessageMarkdownWorkspaceAppIcon[] {
const seen = new Set(
input.workspaceAppIcons.flatMap((icon) => {
const appId = icon.appId.trim();
const iconUrl = icon.iconUrl?.trim() ?? "";
if (!appId || !iconUrl) {
return [];
}
return [
workspaceAppIconKey(appId, icon.workspaceId?.trim() ?? ""),
workspaceAppIconKey(appId, "")
];
})
);
let next: AgentMessageMarkdownWorkspaceAppIcon[] | null = null;
for (const command of input.commands) {
const source = commandAppSource(command);
if (!source) {
continue;
}
const appId = stringValue(source.appId).trim();
const iconUrl = stringValue(source.iconUrl).trim();
if (!appId || !iconUrl) {
continue;
}
const key = workspaceAppIconKey(appId, input.workspaceId);
if (seen.has(key)) {
continue;
}
if (!next) {
next = [...input.workspaceAppIcons];
}
next.push({
appId,
iconUrl,
workspaceId: input.workspaceId
});
seen.add(key);
}
return next ?? input.workspaceAppIcons;
}