Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
92018edd26 chore(libs/deps): bump the production-dependencies group
Bumps the production-dependencies group in /autogpt_platform/autogpt_libs with 2 updates: [fastapi](https://github.com/fastapi/fastapi) and [pydantic-settings](https://github.com/pydantic/pydantic-settings).


Updates `fastapi` from 0.128.7 to 0.129.0
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.128.7...0.129.0)

Updates `pydantic-settings` from 2.12.0 to 2.13.0
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.12.0...v2.13.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.129.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: pydantic-settings
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:26:23 +00:00
18 changed files with 106 additions and 423 deletions

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "annotated-doc"
@@ -67,7 +67,7 @@ description = "Backport of asyncio.Runner, a context manager that controls event
optional = false
python-versions = "<3.11,>=3.8"
groups = ["dev"]
markers = "python_version < \"3.11\""
markers = "python_version == \"3.10\""
files = [
{file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"},
{file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"},
@@ -541,7 +541,7 @@ description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main", "dev"]
markers = "python_version < \"3.11\""
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
@@ -570,14 +570,14 @@ tests = ["coverage", "coveralls", "dill", "mock", "nose"]
[[package]]
name = "fastapi"
version = "0.128.7"
version = "0.129.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "fastapi-0.128.7-py3-none-any.whl", hash = "sha256:6bd9bd31cb7047465f2d3fa3ba3f33b0870b17d4eaf7cdb36d1576ab060ad662"},
{file = "fastapi-0.128.7.tar.gz", hash = "sha256:783c273416995486c155ad2c0e2b45905dedfaf20b9ef8d9f6a9124670639a24"},
{file = "fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec"},
{file = "fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af"},
]
[package.dependencies]
@@ -1863,14 +1863,14 @@ typing-extensions = ">=4.14.1"
[[package]]
name = "pydantic-settings"
version = "2.12.0"
version = "2.13.0"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"},
{file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"},
{file = "pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246"},
{file = "pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe"},
]
[package.dependencies]
@@ -2564,7 +2564,7 @@ description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version < \"3.11\""
markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -2912,4 +2912,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "9619cae908ad38fa2c48016a58bcf4241f6f5793aa0e6cc140276e91c433cbbb"
content-hash = "78c5c80fd684fcb054d2a7a55c30a7b8c056f52df277ef0225dc07e0865bc7ec"

View File

@@ -11,11 +11,11 @@ python = ">=3.10,<4.0"
colorama = "^0.4.6"
cryptography = "^46.0"
expiringdict = "^1.2.2"
fastapi = "^0.128.7"
fastapi = "^0.129.0"
google-cloud-logging = "^3.13.0"
launchdarkly-server-sdk = "^9.15.0"
pydantic = "^2.12.5"
pydantic-settings = "^2.12.0"
pydantic-settings = "^2.13.0"
pyjwt = { version = "^2.11.0", extras = ["crypto"] }
redis = "^6.2.0"
supabase = "^2.28.0"

View File

@@ -104,8 +104,8 @@ def _get_linear_config() -> tuple[LinearClient, str, str]:
Raises RuntimeError if any required setting is missing.
"""
secrets = _get_settings().secrets
if not secrets.copilot_linear_api_key:
raise RuntimeError("COPILOT_LINEAR_API_KEY is not configured")
if not secrets.linear_api_key:
raise RuntimeError("LINEAR_API_KEY is not configured")
if not secrets.linear_feature_request_project_id:
raise RuntimeError("LINEAR_FEATURE_REQUEST_PROJECT_ID is not configured")
if not secrets.linear_feature_request_team_id:
@@ -114,7 +114,7 @@ def _get_linear_config() -> tuple[LinearClient, str, str]:
credentials = APIKeyCredentials(
id="system-linear",
provider="linear",
api_key=SecretStr(secrets.copilot_linear_api_key),
api_key=SecretStr(secrets.linear_api_key),
title="System Linear API Key",
)
client = LinearClient(credentials=credentials)

View File

@@ -662,7 +662,7 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
mem0_api_key: str = Field(default="", description="Mem0 API key")
elevenlabs_api_key: str = Field(default="", description="ElevenLabs API key")
copilot_linear_api_key: str = Field(
linear_api_key: str = Field(
default="", description="Linear API key for system-level operations"
)
linear_feature_request_project_id: str = Field(

View File

@@ -62,6 +62,7 @@
"@rjsf/validator-ajv8": "6.1.2",
"@sentry/nextjs": "10.27.0",
"@streamdown/cjk": "1.0.1",
"@streamdown/code": "1.0.1",
"@streamdown/math": "1.0.1",
"@streamdown/mermaid": "1.0.1",
"@supabase/ssr": "0.7.0",
@@ -115,7 +116,6 @@
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"shepherd.js": "14.5.1",
"shiki": "^3.21.0",
"sonner": "2.0.7",
"streamdown": "2.1.0",
"tailwind-merge": "2.6.0",

View File

@@ -108,6 +108,9 @@ importers:
'@streamdown/cjk':
specifier: 1.0.1
version: 1.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@18.3.1)(unified@11.0.5)
'@streamdown/code':
specifier: 1.0.1
version: 1.0.1(react@18.3.1)
'@streamdown/math':
specifier: 1.0.1
version: 1.0.1(react@18.3.1)
@@ -267,9 +270,6 @@ importers:
shepherd.js:
specifier: 14.5.1
version: 14.5.1
shiki:
specifier: ^3.21.0
version: 3.21.0
sonner:
specifier: 2.0.7
version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3307,6 +3307,11 @@ packages:
peerDependencies:
react: ^18.0.0 || ^19.0.0
'@streamdown/code@1.0.1':
resolution: {integrity: sha512-U9LITfQ28tZYAoY922jdtw1ryg4kgRBdURopqK9hph7G2fBUwPeHthjH7SvaV0fvFv7EqjqCzARJuWUljLe9Ag==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
'@streamdown/math@1.0.1':
resolution: {integrity: sha512-R9WdHbpERiRU7WeO7oT1aIbnLJ/jraDr89F7X9x2OM//Y8G8UMATRnLD/RUwg4VLr8Nu7QSIJ0Pa8lXd2meM4Q==}
peerDependencies:
@@ -11902,6 +11907,11 @@ snapshots:
- micromark-util-types
- unified
'@streamdown/code@1.0.1(react@18.3.1)':
dependencies:
react: 18.3.1
shiki: 3.21.0
'@streamdown/math@1.0.1(react@18.3.1)':
dependencies:
katex: 0.16.28

View File

@@ -1,16 +1,10 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { SidebarProvider } from "@/components/ui/sidebar";
import { DotsThree } from "@phosphor-icons/react";
// TODO: Replace with modern Dialog component when available
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
import { DeleteChatDialog } from "./components/DeleteChatDialog/DeleteChatDialog";
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
import { MobileHeader } from "./components/MobileHeader/MobileHeader";
import { ScaleLoader } from "./components/ScaleLoader/ScaleLoader";
@@ -62,7 +56,19 @@ export function CopilotPage() {
>
{!isMobile && <ChatSidebar />}
<div className="relative flex h-full w-full flex-col overflow-hidden bg-[#f8f8f9] px-0">
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
{isMobile && (
<MobileHeader
onOpenDrawer={handleOpenDrawer}
showDelete={!!sessionId}
isDeleting={isDeleting}
onDelete={() => {
const session = sessions.find((s) => s.id === sessionId);
if (session) {
handleDeleteClick(session.id, session.title);
}
}}
/>
)}
<div className="flex-1 overflow-hidden">
<ChatContainer
messages={messages}
@@ -74,38 +80,6 @@ export function CopilotPage() {
onCreateSession={createSession}
onSend={onSend}
onStop={stop}
headerSlot={
isMobile && sessionId ? (
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="rounded p-1.5 hover:bg-neutral-100"
aria-label="More actions"
>
<DotsThree className="h-5 w-5 text-neutral-600" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
const session = sessions.find(
(s) => s.id === sessionId,
);
if (session) {
handleDeleteClick(session.id, session.title);
}
}}
disabled={isDeleting}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
Delete chat
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : undefined
}
/>
</div>
</div>
@@ -123,11 +97,12 @@ export function CopilotPage() {
)}
{/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */}
{isMobile && (
<DeleteChatDialog
session={sessionToDelete}
isDeleting={isDeleting}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
<DeleteConfirmDialog
entityType="chat"
entityName={sessionToDelete?.title || "Untitled chat"}
open={!!sessionToDelete}
onOpenChange={(open) => !open && handleCancelDelete()}
onDoDelete={handleConfirmDelete}
/>
)}
</SidebarProvider>

View File

@@ -2,7 +2,6 @@
import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput";
import { UIDataTypes, UIMessage, UITools } from "ai";
import { LayoutGroup, motion } from "framer-motion";
import { ReactNode } from "react";
import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer";
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
import { EmptySession } from "../EmptySession/EmptySession";
@@ -17,7 +16,6 @@ export interface ChatContainerProps {
onCreateSession: () => void | Promise<string>;
onSend: (message: string) => void | Promise<void>;
onStop: () => void;
headerSlot?: ReactNode;
}
export const ChatContainer = ({
messages,
@@ -29,7 +27,6 @@ export const ChatContainer = ({
onCreateSession,
onSend,
onStop,
headerSlot,
}: ChatContainerProps) => {
const inputLayoutId = "copilot-2-chat-input";
@@ -44,7 +41,6 @@ export const ChatContainer = ({
status={status}
error={error}
isLoading={isLoadingSession}
headerSlot={headerSlot}
/>
<motion.div
initial={{ opacity: 0 }}

View File

@@ -118,7 +118,6 @@ interface ChatMessagesContainerProps {
status: string;
error: Error | undefined;
isLoading: boolean;
headerSlot?: React.ReactNode;
}
export const ChatMessagesContainer = ({
@@ -126,7 +125,6 @@ export const ChatMessagesContainer = ({
status,
error,
isLoading,
headerSlot,
}: ChatMessagesContainerProps) => {
const [thinkingPhrase, setThinkingPhrase] = useState(getRandomPhrase);
const lastToastTimeRef = useRef(0);
@@ -167,7 +165,6 @@ export const ChatMessagesContainer = ({
return (
<Conversation className="min-h-0 flex-1">
<ConversationContent className="flex flex-1 flex-col gap-6 px-3 py-6">
{headerSlot}
{isLoading && messages.length === 0 && (
<div className="flex min-h-full flex-1 items-center justify-center">
<LoadingSpinner className="text-neutral-600" />

View File

@@ -7,13 +7,9 @@ import {
import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { toast } from "@/components/molecules/Toast/use-toast";
// TODO: Replace with modern Dialog component when available
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
import {
Sidebar,
SidebarContent,
@@ -23,12 +19,11 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { parseAsString, useQueryState } from "nuqs";
import { useState } from "react";
import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog";
import { parseAsString, useQueryState } from "nuqs";
export function ChatSidebar() {
const { state } = useSidebar();
@@ -97,12 +92,6 @@ export function ChatSidebar() {
}
}
function handleCancelDelete() {
if (!isDeleting) {
setSessionToDelete(null);
}
}
function formatDate(dateString: string) {
const date = new Date(dateString);
const now = new Date();
@@ -231,28 +220,16 @@ export function ChatSidebar() {
</Text>
</div>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full p-1.5 text-zinc-600 transition-all hover:bg-neutral-100"
aria-label="More actions"
>
<DotsThree className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) =>
handleDeleteClick(e, session.id, session.title)
}
disabled={isDeleting}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
Delete chat
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<button
onClick={(e) =>
handleDeleteClick(e, session.id, session.title)
}
disabled={isDeleting}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1.5 text-zinc-400 opacity-0 transition-all group-hover:opacity-100 hover:bg-red-100 hover:text-red-600 focus-visible:opacity-100 disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Delete chat"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
))
)}
@@ -280,11 +257,12 @@ export function ChatSidebar() {
)}
</Sidebar>
<DeleteChatDialog
session={sessionToDelete}
isDeleting={isDeleting}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
<DeleteConfirmDialog
entityType="chat"
entityName={sessionToDelete?.title || "Untitled chat"}
open={!!sessionToDelete}
onOpenChange={(open) => !open && setSessionToDelete(null)}
onDoDelete={handleConfirmDelete}
/>
</>
);

View File

@@ -1,57 +0,0 @@
"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 (
<Dialog
title="Delete chat"
styling={{ maxWidth: "30rem", minWidth: "auto" }}
controlled={{
isOpen: !!session,
set: async (open) => {
if (!open && !isDeleting) {
onCancel();
}
},
}}
onClose={isDeleting ? undefined : onCancel}
>
<Dialog.Content>
<Text variant="body">
Are you sure you want to delete{" "}
<Text variant="body-medium" as="span">
&quot;{session?.title || "Untitled chat"}&quot;
</Text>
? This action cannot be undone.
</Text>
<Dialog.Footer>
<Button variant="secondary" onClick={onCancel} disabled={isDeleting}>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirm}
loading={isDeleting}
>
Delete
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,12 +1,20 @@
import { Button } from "@/components/atoms/Button/Button";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import { ListIcon } from "@phosphor-icons/react";
import { ListIcon, TrashIcon } from "@phosphor-icons/react";
interface Props {
onOpenDrawer: () => void;
showDelete?: boolean;
isDeleting?: boolean;
onDelete?: () => void;
}
export function MobileHeader({ onOpenDrawer }: Props) {
export function MobileHeader({
onOpenDrawer,
showDelete,
isDeleting,
onDelete,
}: Props) {
return (
<div
className="fixed z-50 flex gap-2"
@@ -21,6 +29,18 @@ export function MobileHeader({ onOpenDrawer }: Props) {
>
<ListIcon width="1.25rem" height="1.25rem" />
</Button>
{showDelete && onDelete && (
<Button
variant="icon"
size="icon"
aria-label="Delete current chat"
onClick={onDelete}
disabled={isDeleting}
className="bg-white text-red-500 shadow-md hover:bg-red-50 hover:text-red-600 disabled:opacity-50"
>
<TrashIcon width="1.25rem" height="1.25rem" />
</Button>
)}
</div>
);
}

View File

@@ -192,10 +192,8 @@ export function useCopilotPage() {
}, [sessionToDelete, deleteSessionMutation]);
const handleCancelDelete = useCallback(() => {
if (!isDeleting) {
setSessionToDelete(null);
}
}, [isDeleting]);
setSessionToDelete(null);
}, []);
return {
sessionId,

View File

@@ -10,7 +10,7 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { cjk } from "@streamdown/cjk";
import { code } from "@/lib/streamdown-code-plugin";
import { code } from "@streamdown/code";
import { math } from "@streamdown/math";
import { mermaid } from "@streamdown/mermaid";
import type { UIMessage } from "ai";

View File

@@ -25,7 +25,7 @@ export function BaseFooter({
</div>
) : (
<div
className={`flex w-full items-end justify-end gap-4 pt-6 ${className}`}
className={`flex w-full items-end justify-between gap-4 pt-6 ${className}`}
data-testid={testId}
>
{children}

View File

@@ -1,70 +0,0 @@
import {
bundledLanguages,
bundledLanguagesInfo,
createHighlighter,
type BundledLanguage,
type BundledTheme,
type HighlighterGeneric,
} from "shiki";
export type { BundledLanguage, BundledTheme };
const LANGUAGE_ALIASES: Record<string, string> = 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<BundledLanguage, BundledTheme>
> | null = null;
export function getShikiHighlighter(): Promise<
HighlighterGeneric<BundledLanguage, BundledTheme>
> {
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[];
}

View File

@@ -1,159 +0,0 @@
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<string, string>;
}[][];
fg?: string;
bg?: string;
}
type HighlightCallback = (result: HighlightResult) => void;
const MAX_CACHE_SIZE = 500;
const tokenCache = new Map<string, HighlightResult>();
const pendingCallbacks = new Map<string, Set<HighlightCallback>>();
const inFlightLanguageLoads = new Map<string, Promise<void>>();
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();

View File

@@ -465,13 +465,9 @@ export async function navigateToAgentByName(
export async function clickRunButton(page: Page): Promise<void> {
const { getId } = getSelectors(page);
// 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");
// Wait for page to stabilize and buttons to render
// The NewAgentLibraryView shows either "Setup your task" (empty state)
// or "New task" (with items) button
const setupTaskButton = page.getByRole("button", {
name: /Setup your task/i,
});
@@ -479,7 +475,8 @@ export async function clickRunButton(page: Page): Promise<void> {
const runButton = getId("agent-run-button");
const runAgainButton = getId("run-again-button");
// Wait for any of the buttons to appear
// Use Promise.race with waitFor to wait for any of the buttons to appear
// This handles the async rendering in CI environments
try {
await Promise.race([
setupTaskButton.waitFor({ state: "visible", timeout: 15000 }),
@@ -493,7 +490,7 @@ export async function clickRunButton(page: Page): Promise<void> {
);
}
// Check which button is visible and click it
// Now check which button is visible and click it
if (await setupTaskButton.isVisible()) {
await setupTaskButton.click();
const startTaskButton = page
@@ -537,9 +534,7 @@ export async function runAgent(page: Page): Promise<void> {
export async function waitForAgentPageLoad(page: Page): Promise<void> {
await page.waitForURL(/.*\/library\/agents\/[^/]+/);
// Wait for sidebar data to finish loading so the page settles
// into its final state (empty view vs sidebar view)
await page.waitForLoadState("networkidle");
await page.getByTestId("Run actions").isVisible({ timeout: 10000 });
}
export async function getAgentName(page: Page): Promise<string> {