!open && setSessionToDelete(null)}
- onDoDelete={handleConfirmDelete}
+
>
);
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx
new file mode 100644
index 0000000000..d94625c4e3
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { Button } from "@/components/atoms/Button/Button";
+import { Text } from "@/components/atoms/Text/Text";
+import { Dialog } from "@/components/molecules/Dialog/Dialog";
+
+interface Props {
+ session: { id: string; title: string | null | undefined } | null;
+ isDeleting: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+export function DeleteChatDialog({
+ session,
+ isDeleting,
+ onConfirm,
+ onCancel,
+}: Props) {
+ return (
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx
index b4b7636c81..bed9989244 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx
@@ -1,20 +1,12 @@
import { Button } from "@/components/atoms/Button/Button";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
-import { ListIcon, TrashIcon } from "@phosphor-icons/react";
+import { ListIcon } from "@phosphor-icons/react";
interface Props {
onOpenDrawer: () => void;
- showDelete?: boolean;
- isDeleting?: boolean;
- onDelete?: () => void;
}
-export function MobileHeader({
- onOpenDrawer,
- showDelete,
- isDeleting,
- onDelete,
-}: Props) {
+export function MobileHeader({ onOpenDrawer }: Props) {
return (
- {showDelete && onDelete && (
-
- )}
);
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
index 444e745ec6..a0f0a2b7fd 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
@@ -192,8 +192,10 @@ export function useCopilotPage() {
}, [sessionToDelete, deleteSessionMutation]);
const handleCancelDelete = useCallback(() => {
- setSessionToDelete(null);
- }, []);
+ if (!isDeleting) {
+ setSessionToDelete(null);
+ }
+ }, [isDeleting]);
return {
sessionId,
diff --git a/autogpt_platform/frontend/src/components/ai-elements/message.tsx b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
index 5cc330e57c..cc11c2f01b 100644
--- a/autogpt_platform/frontend/src/components/ai-elements/message.tsx
+++ b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
@@ -10,7 +10,7 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { cjk } from "@streamdown/cjk";
-import { code } from "@streamdown/code";
+import { code } from "@/lib/streamdown-code-plugin";
import { math } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import type { UIMessage } from "ai";
diff --git a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx
index 3ed1157b6c..1951525ccd 100644
--- a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx
+++ b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx
@@ -25,7 +25,7 @@ export function BaseFooter({
) : (
{children}
diff --git a/autogpt_platform/frontend/src/lib/shiki-highlighter.ts b/autogpt_platform/frontend/src/lib/shiki-highlighter.ts
new file mode 100644
index 0000000000..f6bbcb9b94
--- /dev/null
+++ b/autogpt_platform/frontend/src/lib/shiki-highlighter.ts
@@ -0,0 +1,70 @@
+import {
+ bundledLanguages,
+ bundledLanguagesInfo,
+ createHighlighter,
+ type BundledLanguage,
+ type BundledTheme,
+ type HighlighterGeneric,
+} from "shiki";
+
+export type { BundledLanguage, BundledTheme };
+
+const LANGUAGE_ALIASES: Record = Object.fromEntries(
+ bundledLanguagesInfo.flatMap((lang) =>
+ (lang.aliases ?? []).map((alias) => [alias, lang.id]),
+ ),
+);
+
+const SUPPORTED_LANGUAGES = new Set(Object.keys(bundledLanguages));
+
+const PRELOAD_LANGUAGES: BundledLanguage[] = [
+ "javascript",
+ "typescript",
+ "python",
+ "json",
+ "bash",
+ "yaml",
+ "markdown",
+ "html",
+ "css",
+ "sql",
+ "tsx",
+ "jsx",
+];
+
+export const SHIKI_THEMES: [BundledTheme, BundledTheme] = [
+ "github-light",
+ "github-dark",
+];
+
+let highlighterPromise: Promise<
+ HighlighterGeneric
+> | null = null;
+
+export function getShikiHighlighter(): Promise<
+ HighlighterGeneric
+> {
+ if (!highlighterPromise) {
+ highlighterPromise = createHighlighter({
+ themes: SHIKI_THEMES,
+ langs: PRELOAD_LANGUAGES,
+ }).catch((err) => {
+ highlighterPromise = null;
+ throw err;
+ });
+ }
+ return highlighterPromise;
+}
+
+export function resolveLanguage(lang: string): string {
+ const normalized = lang.trim().toLowerCase();
+ return LANGUAGE_ALIASES[normalized] ?? normalized;
+}
+
+export function isLanguageSupported(lang: string): boolean {
+ return SUPPORTED_LANGUAGES.has(resolveLanguage(lang));
+}
+
+export function getSupportedLanguages(): BundledLanguage[] {
+ return Array.from(SUPPORTED_LANGUAGES) as BundledLanguage[];
+}
diff --git a/autogpt_platform/frontend/src/lib/streamdown-code-plugin.ts b/autogpt_platform/frontend/src/lib/streamdown-code-plugin.ts
new file mode 100644
index 0000000000..52ccc5afed
--- /dev/null
+++ b/autogpt_platform/frontend/src/lib/streamdown-code-plugin.ts
@@ -0,0 +1,159 @@
+import type { CodeHighlighterPlugin } from "streamdown";
+
+import {
+ type BundledLanguage,
+ type BundledTheme,
+ getShikiHighlighter,
+ getSupportedLanguages,
+ isLanguageSupported,
+ resolveLanguage,
+ SHIKI_THEMES,
+} from "./shiki-highlighter";
+
+interface HighlightResult {
+ tokens: {
+ content: string;
+ color?: string;
+ htmlStyle?: Record;
+ }[][];
+ fg?: string;
+ bg?: string;
+}
+
+type HighlightCallback = (result: HighlightResult) => void;
+
+const MAX_CACHE_SIZE = 500;
+const tokenCache = new Map();
+const pendingCallbacks = new Map>();
+const inFlightLanguageLoads = new Map>();
+
+function simpleHash(str: string): string {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash;
+ }
+ return hash.toString(36);
+}
+
+function getCacheKey(
+ code: string,
+ lang: string,
+ themes: readonly string[],
+): string {
+ return `${lang}:${themes.join(",")}:${simpleHash(code)}`;
+}
+
+function evictOldestIfNeeded(): void {
+ if (tokenCache.size > MAX_CACHE_SIZE) {
+ const oldestKey = tokenCache.keys().next().value;
+ if (oldestKey) {
+ tokenCache.delete(oldestKey);
+ }
+ }
+}
+
+export function createSingletonCodePlugin(): CodeHighlighterPlugin {
+ return {
+ name: "shiki",
+ type: "code-highlighter",
+
+ supportsLanguage(lang: BundledLanguage): boolean {
+ return isLanguageSupported(lang);
+ },
+
+ getSupportedLanguages(): BundledLanguage[] {
+ return getSupportedLanguages();
+ },
+
+ getThemes(): [BundledTheme, BundledTheme] {
+ return SHIKI_THEMES;
+ },
+
+ highlight({ code, language, themes }, callback) {
+ const lang = resolveLanguage(language);
+ const cacheKey = getCacheKey(code, lang, themes);
+
+ if (tokenCache.has(cacheKey)) {
+ return tokenCache.get(cacheKey)!;
+ }
+
+ if (callback) {
+ if (!pendingCallbacks.has(cacheKey)) {
+ pendingCallbacks.set(cacheKey, new Set());
+ }
+ pendingCallbacks.get(cacheKey)!.add(callback);
+ }
+
+ getShikiHighlighter()
+ .then(async (highlighter) => {
+ const loadedLanguages = highlighter.getLoadedLanguages();
+
+ if (!loadedLanguages.includes(lang) && isLanguageSupported(lang)) {
+ let loadPromise = inFlightLanguageLoads.get(lang);
+ if (!loadPromise) {
+ loadPromise = highlighter
+ .loadLanguage(lang as BundledLanguage)
+ .finally(() => {
+ inFlightLanguageLoads.delete(lang);
+ });
+ inFlightLanguageLoads.set(lang, loadPromise);
+ }
+ await loadPromise;
+ }
+
+ const finalLang = (
+ highlighter.getLoadedLanguages().includes(lang) ? lang : "text"
+ ) as BundledLanguage;
+
+ const shikiResult = highlighter.codeToTokens(code, {
+ lang: finalLang,
+ themes: { light: themes[0], dark: themes[1] },
+ });
+
+ const result: HighlightResult = {
+ tokens: shikiResult.tokens.map((line) =>
+ line.map((token) => ({
+ content: token.content,
+ color: token.color,
+ htmlStyle: token.htmlStyle,
+ })),
+ ),
+ fg: shikiResult.fg,
+ bg: shikiResult.bg,
+ };
+
+ evictOldestIfNeeded();
+ tokenCache.set(cacheKey, result);
+
+ const callbacks = pendingCallbacks.get(cacheKey);
+ if (callbacks) {
+ callbacks.forEach((cb) => {
+ cb(result);
+ });
+ pendingCallbacks.delete(cacheKey);
+ }
+ })
+ .catch((error) => {
+ console.error("[Shiki] Failed to highlight code:", error);
+
+ const fallback: HighlightResult = {
+ tokens: code.split("\n").map((line) => [{ content: line }]),
+ };
+
+ const callbacks = pendingCallbacks.get(cacheKey);
+ if (callbacks) {
+ callbacks.forEach((cb) => {
+ cb(fallback);
+ });
+ pendingCallbacks.delete(cacheKey);
+ }
+ });
+
+ return null;
+ },
+ };
+}
+
+export const code = createSingletonCodePlugin();
diff --git a/autogpt_platform/frontend/src/tests/pages/library.page.ts b/autogpt_platform/frontend/src/tests/pages/library.page.ts
index 03e98598b4..17a265d590 100644
--- a/autogpt_platform/frontend/src/tests/pages/library.page.ts
+++ b/autogpt_platform/frontend/src/tests/pages/library.page.ts
@@ -465,9 +465,13 @@ export async function navigateToAgentByName(
export async function clickRunButton(page: Page): Promise {
const { getId } = getSelectors(page);
- // Wait for page to stabilize and buttons to render
- // The NewAgentLibraryView shows either "Setup your task" (empty state)
- // or "New task" (with items) button
+ // Wait for sidebar loading to complete before detecting buttons.
+ // During sidebar loading, the "New task" button appears transiently
+ // even for agents with no items, then switches to "Setup your task"
+ // once loading finishes. Waiting for network idle ensures the page
+ // has settled into its final state.
+ await page.waitForLoadState("networkidle");
+
const setupTaskButton = page.getByRole("button", {
name: /Setup your task/i,
});
@@ -475,8 +479,7 @@ export async function clickRunButton(page: Page): Promise {
const runButton = getId("agent-run-button");
const runAgainButton = getId("run-again-button");
- // Use Promise.race with waitFor to wait for any of the buttons to appear
- // This handles the async rendering in CI environments
+ // Wait for any of the buttons to appear
try {
await Promise.race([
setupTaskButton.waitFor({ state: "visible", timeout: 15000 }),
@@ -490,7 +493,7 @@ export async function clickRunButton(page: Page): Promise {
);
}
- // Now check which button is visible and click it
+ // Check which button is visible and click it
if (await setupTaskButton.isVisible()) {
await setupTaskButton.click();
const startTaskButton = page
@@ -534,7 +537,9 @@ export async function runAgent(page: Page): Promise {
export async function waitForAgentPageLoad(page: Page): Promise {
await page.waitForURL(/.*\/library\/agents\/[^/]+/);
- await page.getByTestId("Run actions").isVisible({ timeout: 10000 });
+ // Wait for sidebar data to finish loading so the page settles
+ // into its final state (empty view vs sidebar view)
+ await page.waitForLoadState("networkidle");
}
export async function getAgentName(page: Page): Promise {