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

189 lines
5.6 KiB
TypeScript

import claudeVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/claude-vinyl.png";
import codexVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/codex-vinyl.png";
import cursorVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/cursor-vinyl.png";
import openclawVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/openclaw-vinyl.png";
import opencodeVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/opencode-vinyl.png";
import tuttiVinylAssetUrl from "../../app/renderer/assets/icons/agent-vinyls/tutti-vinyl.png";
import {
agentGuiScheduler,
type AgentGuiScheduledTask
} from "./agentGuiScheduler";
import type { AgentGUIAgentAvatarPresentation } from "./model/agentGuiAgentAvatarPresentation";
const AGENT_VINYL_COVER_BY_PROVIDER: Readonly<Record<string, string>> = {
"claude-code": claudeVinylAssetUrl,
codex: codexVinylAssetUrl,
cursor: cursorVinylAssetUrl,
openclaw: openclawVinylAssetUrl,
opencode: opencodeVinylAssetUrl,
"tutti-agent": tuttiVinylAssetUrl
};
const BADGE_IMAGE_RETRY_DELAYS_MS = [150, 500] as const;
export interface AgentGuiHeroCarouselDecodedImages {
badges: readonly (HTMLImageElement | null)[];
covers: readonly (HTMLImageElement | null)[];
icons: readonly (HTMLImageElement | null)[];
}
interface PendingImageLoad {
cancel(): void;
promise: Promise<HTMLImageElement | null>;
}
export class AgentGuiHeroCarouselImageLoad {
readonly result: Promise<AgentGuiHeroCarouselDecodedImages>;
private readonly pendingLoads = new Set<PendingImageLoad>();
private canceled = false;
constructor(items: readonly AgentGUIAgentAvatarPresentation[]) {
this.result = Promise.all(
items.map(async (item) => {
const [icon, cover, badge] = await Promise.all([
this.loadImage(item.iconUrl),
this.loadImage(
item.heroImageUrl?.trim() ||
AGENT_VINYL_COVER_BY_PROVIDER[item.provider] ||
null
),
this.loadImage(item.badge?.iconUrl ?? null, true)
]);
return { badge, cover, icon };
})
).then((entries) => ({
badges: entries.map((entry) => entry.badge),
covers: entries.map((entry) => entry.cover),
icons: entries.map((entry) => entry.icon)
}));
}
cancel(): void {
if (this.canceled) {
return;
}
this.canceled = true;
for (const pending of this.pendingLoads) {
pending.cancel();
}
this.pendingLoads.clear();
}
private loadImage(
url: string | null,
retryTransientFailure = false
): Promise<HTMLImageElement | null> {
if (!url || this.canceled || typeof Image !== "function") {
return Promise.resolve(null);
}
let settled = false;
let image: HTMLImageElement | null = null;
let retryIndex = 0;
let retryTask: AgentGuiScheduledTask | null = null;
let resolvePromise = (_value: HTMLImageElement | null): void => undefined;
const pending: PendingImageLoad = {
cancel: () => settle(null, true),
promise: new Promise((resolve) => {
resolvePromise = resolve;
})
};
const settle = (
value: HTMLImageElement | null,
clearSource = false
): void => {
if (settled) {
return;
}
settled = true;
if (retryTask !== null) {
retryTask.cancel();
retryTask = null;
}
if (image) {
image.onload = null;
image.onerror = null;
}
this.pendingLoads.delete(pending);
if (clearSource && image) {
image.src = "";
}
resolvePromise(value);
};
const settleDecoded = (loadedImage: HTMLImageElement): void => {
let decode: Promise<void> | undefined;
try {
decode = loadedImage.decode?.();
} catch {
settle(loadedImage);
return;
}
if (decode) {
void decode
.then(() => settle(loadedImage))
.catch(() => settle(loadedImage));
return;
}
settle(loadedImage);
};
const retryOrSettle = (failedImage: HTMLImageElement): void => {
failedImage.onload = null;
failedImage.onerror = null;
if (
retryTransientFailure &&
retryIndex < BADGE_IMAGE_RETRY_DELAYS_MS.length &&
!this.canceled
) {
failedImage.src = "";
const delay = BADGE_IMAGE_RETRY_DELAYS_MS[retryIndex]!;
retryIndex += 1;
retryTask = agentGuiScheduler.schedule(delay, () => {
retryTask = null;
startAttempt();
});
return;
}
settle(null);
};
const startAttempt = (): void => {
if (settled || this.canceled) {
settle(null, true);
return;
}
const nextImage = new Image();
image = nextImage;
nextImage.crossOrigin = "anonymous";
nextImage.decoding = "async";
nextImage.loading = "eager";
nextImage.setAttribute("fetchpriority", "high");
let attemptFinished = false;
const handleLoaded = (): void => {
if (attemptFinished) {
return;
}
attemptFinished = true;
settleDecoded(nextImage);
};
const handleFailed = (): void => {
if (attemptFinished) {
return;
}
attemptFinished = true;
retryOrSettle(nextImage);
};
nextImage.onload = handleLoaded;
nextImage.onerror = handleFailed;
nextImage.src = url;
if (nextImage.complete) {
if (nextImage.naturalWidth > 0) {
handleLoaded();
} else {
handleFailed();
}
}
};
this.pendingLoads.add(pending);
startAttempt();
return pending.promise;
}
}