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

248 lines
7.6 KiB
TypeScript

import type { JSX } from "react";
import { translate } from "../../../i18n/index";
import type { AgentMessageContentVM } from "../contracts/agentMessageRowVM";
import { AgentMessageDetailsDisclosure } from "./AgentMessageDetailsDisclosure";
import agentSystemNoticeStyles from "./agentSystemNoticeStyles";
import { useElapsedSeconds } from "./useElapsedSeconds";
const TRANSPORT_RETRY_PROGRESS_PATTERN =
/\b(reconnect(?:ing)?(?:\s*(?:\.\.\.|…|[.。]+|:|-))?\s*\(?\d+\s*\/\s*\d+\)?)/i;
export function AgentSystemNoticeMessage({
message
}: {
message: AgentMessageContentVM;
}): JSX.Element {
"use memo";
const notice = message.systemNotice;
const detail = notice?.detail?.trim() ?? "";
const title = systemNoticeTitle(message);
if (notice?.noticeKind === "transport_retry") {
const retryText = transportRetryNoticeText(message);
return (
<div
role="status"
className="box-border w-full min-w-0 py-1 text-[13px] leading-5 text-[var(--text-secondary)]"
>
{retryText}
</div>
);
}
if (isContextCompactionProgressNotice(message)) {
return (
<ContextCompactionProgressDivider
startedAtUnixMs={message.occurredAtUnixMs}
/>
);
}
if (isContextCompactionNotice(message)) {
return (
<ContextCompactionDivider
text={translate("agentHost.agentGui.contextCompactionCompleted")}
/>
);
}
if (isContextHandoffRequiredNotice(message)) {
return (
<section
role="alert"
className={`box-border w-full min-w-0 rounded-[8px] border p-3 text-[13px] leading-5 text-[var(--text-primary)] ${agentSystemNoticeStyles.contextHandoff}`}
>
<div className="font-medium text-[var(--state-danger)]">
{translate("agentHost.agentGui.contextHandoffRequired")}
</div>
<div className="mt-1 text-[var(--text-secondary)]">
{translate("agentHost.agentGui.contextHandoffRequiredDetail")}
</div>
{detail ? (
<AgentMessageDetailsDisclosure detail={detail} className="mt-2" />
) : null}
</section>
);
}
if (isContextCompactionInterruptedNotice(message)) {
return (
<ContextCompactionDivider
text={translate("agentHost.agentGui.contextCompactionInterrupted")}
detail={detail || null}
/>
);
}
const isStatusNotice = systemNoticeIsStatus(message);
return (
<section
role={isStatusNotice ? "status" : undefined}
className={`box-border w-full min-w-0 rounded-[8px] border p-3 text-[13px] leading-5 text-[var(--text-secondary)] ${agentSystemNoticeStyles.routine}`}
>
<div className="min-w-0">
<div className="font-medium text-[var(--text-secondary)]">{title}</div>
{detail ? (
<AgentMessageDetailsDisclosure
detail={detail}
className="mt-1"
tone="muted"
/>
) : null}
</div>
</section>
);
}
function systemNoticeIsStatus(message: AgentMessageContentVM): boolean {
const notice = message.systemNotice;
return (
notice?.severity === "warning" ||
notice?.severity === "error" ||
notice?.noticeKind === "transport_fallback" ||
isTransportFallbackNotice(message)
);
}
function isTransportFallbackNotice(message: AgentMessageContentVM): boolean {
const notice = message.systemNotice;
const text = [notice?.title, notice?.detail, message.body]
.filter(Boolean)
.join("\n")
.toLowerCase();
return (
text.includes("falling back from websockets") ||
text.includes("https transport")
);
}
function transportRetryNoticeText(message: AgentMessageContentVM): string {
const notice = message.systemNotice;
const detail = notice?.detail?.trim() ?? "";
const progressText =
transportRetryProgressText(detail) ??
transportRetryProgressText(notice?.title ?? "") ??
transportRetryProgressText(message.body);
if (progressText) {
return progressText;
}
return (
notice?.title?.trim() ||
message.body.trim() ||
translate("agentHost.agentGui.systemNoticeTransportRetry")
);
}
function transportRetryProgressText(value: string): string | null {
const match = TRANSPORT_RETRY_PROGRESS_PATTERN.exec(value.trim());
return match?.[1]?.replace(/\s+/g, " ").trim() || null;
}
function isContextCompactionNotice(message: AgentMessageContentVM): boolean {
const notice = message.systemNotice;
return notice?.command === "compact" && notice.commandStatus === "completed";
}
function isContextCompactionProgressNotice(
message: AgentMessageContentVM
): boolean {
const notice = message.systemNotice;
return notice?.command === "compact" && notice.commandStatus === "running";
}
function isContextCompactionInterruptedNotice(
message: AgentMessageContentVM
): boolean {
const notice = message.systemNotice;
return (
notice?.command === "compact" &&
(notice.commandStatus === "failed" || notice.commandStatus === "canceled")
);
}
function isContextHandoffRequiredNotice(
message: AgentMessageContentVM
): boolean {
return message.systemNotice?.semanticKind === "context-handoff-required";
}
function ContextCompactionDivider({
text,
detail = null
}: {
text: string;
detail?: string | null;
}): JSX.Element {
"use memo";
return (
<div
role="status"
className="box-border w-full min-w-0 py-2 text-[12px] leading-4 text-[var(--text-secondary)]"
>
<div className="flex min-w-0 items-center gap-3">
<span
aria-hidden="true"
className="h-px min-w-4 flex-1 bg-[var(--line-1)]"
/>
<span className="shrink-0 whitespace-nowrap">{text}</span>
<span
aria-hidden="true"
className="h-px min-w-4 flex-1 bg-[var(--line-1)]"
/>
</div>
{detail ? (
<div className="mt-1 min-w-0 whitespace-pre-wrap break-words text-center leading-5">
{detail}
</div>
) : null}
</div>
);
}
// Live compaction banner: the daemon replaces this notice in place with the
// "Context compacted." notice once the provider finishes, so the timer only
// runs while compaction is actually in flight.
function ContextCompactionProgressDivider({
startedAtUnixMs
}: {
startedAtUnixMs: number | null;
}): JSX.Element {
"use memo";
const elapsedSeconds = useElapsedSeconds(startedAtUnixMs);
const label = translate("agentHost.agentGui.contextCompactionInProgress");
const text =
elapsedSeconds === null
? label
: `${label} · ${formatElapsedSeconds(elapsedSeconds)}`;
return <ContextCompactionDivider text={text} />;
}
function formatElapsedSeconds(seconds: number): string {
if (seconds < 60) {
return `${seconds}s`;
}
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
}
function systemNoticeTitle(message: AgentMessageContentVM): string {
const notice = message.systemNotice;
switch (notice?.noticeKind) {
case "transport_retry":
return translate("agentHost.agentGui.systemNoticeTransportRetry");
case "transport_fallback":
return translate("agentHost.agentGui.systemNoticeTransportFallback");
case "plan_implementation_pending_confirmation":
return translate(
"agentHost.agentGui.systemNoticePlanImplementationPendingConfirmation"
);
case "plan_implementation_completed":
return translate(
"agentHost.agentGui.systemNoticePlanImplementationCompleted"
);
case "warning":
return (
notice.title || translate("agentHost.agentGui.systemNoticeWarning")
);
default:
return (
notice?.title ||
message.body ||
translate("agentHost.agentGui.systemNoticeDefault")
);
}
}