Compare commits

...

5 Commits

Author SHA1 Message Date
Bentlybro
149c3b5ce4 fix: add explicit types to avoid implicit any errors 2026-02-13 09:15:35 +00:00
Bentlybro
9f51f9f034 fix(frontend): use singleton shiki highlighter for code blocks
Fixes SENTRY-1051: shiki warning about too many instances.

@streamdown/code creates a new highlighter per language, causing
memory bloat and warnings when chats have multiple code blocks.

This adds a custom code plugin that:
- Uses a single shiki highlighter instance
- Dynamically loads languages on demand
- Pre-loads common languages (js, ts, python, json, etc.)
- Caches results like the original plugin

Drop-in replacement - same interface as @streamdown/code.
2026-02-13 09:14:48 +00:00
Ubbe
e8c50b96d1 fix(frontend): improve CoPilot chat table styling (#12094)
## Summary
- Remove left and right borders from tables rendered in CoPilot chat
- Increase cell padding (py-3 → py-3.5) for better spacing between text
and lines
- Applies to both Streamdown (main chat) and MarkdownRenderer (tool
outputs)

Design feedback from Olivia to make tables "breathe" more.

## Test plan
- [ ] Open CoPilot chat and trigger a response containing a table
- [ ] Verify tables no longer have left/right borders
- [ ] Verify increased spacing between rows
- [ ] Check both light and dark modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- greptile_comment -->

<h2>Greptile Overview</h2>

<details><summary><h3>Greptile Summary</h3></summary>

Improved CoPilot chat table styling by removing left and right borders
and increasing vertical padding from `py-3` to `py-3.5`. Changes apply
to both:
- Streamdown-rendered tables (via CSS selector in `globals.css`)  
- MarkdownRenderer tables (via Tailwind classes)

The changes make tables "breathe" more per design feedback from Olivia.

**Issue Found:**
- The CSS padding value in `globals.css:192` is `0.625rem` (`py-2.5`)
but should be `0.875rem` (`py-3.5`) to match the PR description and the
MarkdownRenderer implementation.
</details>


<details><summary><h3>Confidence Score: 2/5</h3></summary>

- This PR has a logical error that will cause inconsistent table styling
between Streamdown and MarkdownRenderer tables
- The implementation has an inconsistency where the CSS file uses
`py-2.5` padding while the PR description and MarkdownRenderer use
`py-3.5`. This will result in different table padding between the two
rendering systems, contradicting the goal of consistent styling
improvements.
- Pay close attention to `autogpt_platform/frontend/src/app/globals.css`
- the padding value needs to be corrected to match the intended design
</details>


<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-13 09:38:59 +08:00
Ubbe
30e854569a feat(frontend): add exact timestamp tooltip on run timestamps (#12087)
Resolves OPEN-2693: Make exact timestamp of runs accessible through UI.

The NewAgentLibraryView shows relative timestamps ("2 days ago") for
runs and schedules, but unlike the OldAgentLibraryView it didn't show
the exact timestamp on hover. This PR adds a native `title` tooltip so
users can see the full date/time by hovering.

### Changes 🏗️

- Added `descriptionTitle` prop to `SidebarItemCard` that renders as a
`title` attribute on the description text
- `TaskListItem` now passes the exact `run.started_at` timestamp via
`descriptionTitle`
- `ScheduleListItem` now passes the exact `schedule.next_run_time`
timestamp via `descriptionTitle`

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [ ] Open an agent in the library view
- [ ] Hover over a run's relative timestamp (e.g. "2 days ago") and
confirm the full date/time tooltip appears
- [ ] Hover over a schedule's relative timestamp and confirm the full
date/time tooltip appears

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- greptile_comment -->

<h2>Greptile Overview</h2>

<details><summary><h3>Greptile Summary</h3></summary>

Added native tooltip functionality to show exact timestamps in the
library view. The implementation adds a `descriptionTitle` prop to
`SidebarItemCard` that renders as a `title` attribute on the description
text. This allows users to hover over relative timestamps (e.g., "2 days
ago") to see the full date/time.

**Changes:**
- Added optional `descriptionTitle` prop to `SidebarItemCard` component
(SidebarItemCard.tsx:10)
- `TaskListItem` passes `run.started_at` as the tooltip value
(TaskListItem.tsx:84-86)
- `ScheduleListItem` passes `schedule.next_run_time` as the tooltip
value (ScheduleListItem.tsx:32)
- Unrelated fix included: Sentry configuration updated to suppress
cross-origin stylesheet errors (instrumentation-client.ts:25-28)

**Note:** The PR includes two separate commits - the main timestamp
tooltip feature and a Sentry error suppression fix. The PR description
only documents the timestamp feature.
</details>


<details><summary><h3>Confidence Score: 5/5</h3></summary>

- This PR is safe to merge with minimal risk
- The changes are straightforward and limited in scope - adding an
optional prop that forwards a native HTML attribute for tooltip
functionality. The Text component already supports forwarding arbitrary
HTML attributes through its spread operator (...rest), ensuring the
`title` attribute works correctly. Both the timestamp tooltip feature
and the Sentry configuration fix are low-risk improvements with no
breaking changes.
- No files require special attention
</details>


<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant User
    participant TaskListItem
    participant ScheduleListItem
    participant SidebarItemCard
    participant Text
    participant Browser

    User->>TaskListItem: Hover over run timestamp
    TaskListItem->>SidebarItemCard: Pass descriptionTitle (run.started_at)
    SidebarItemCard->>Text: Render with title attribute
    Text->>Browser: Forward title attribute to DOM
    Browser->>User: Display native tooltip with exact timestamp

    User->>ScheduleListItem: Hover over schedule timestamp
    ScheduleListItem->>SidebarItemCard: Pass descriptionTitle (schedule.next_run_time)
    SidebarItemCard->>Text: Render with title attribute
    Text->>Browser: Forward title attribute to DOM
    Browser->>User: Display native tooltip with exact timestamp
```
</details>


<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:38:16 +08:00
Ubbe
301d7cbada fix(frontend): suppress cross-origin stylesheet security error (#12086)
## Summary
- Adds `ignoreErrors` to the Sentry client configuration
(`instrumentation-client.ts`) to filter out `SecurityError:
CSSStyleSheet.cssRules getter: Not allowed to access cross-origin
stylesheet` errors
- These errors are caused by Sentry Replay (rrweb) attempting to
serialize DOM snapshots that include cross-origin stylesheets (from
browser extensions or CDN-loaded CSS)
- This was reported via Sentry on production, occurring on any page when
logged in

## Changes
- **`frontend/instrumentation-client.ts`**: Added `ignoreErrors: [/Not
allowed to access cross-origin stylesheet/]` to `Sentry.init()` config

## Test plan
- [ ] Verify the error no longer appears in Sentry after deployment
- [ ] Verify Sentry Replay still works correctly for other errors
- [ ] Verify no regressions in error tracking (other errors should still
be captured)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- greptile_comment -->

<h2>Greptile Overview</h2>

<details><summary><h3>Greptile Summary</h3></summary>

Adds error filtering to Sentry client configuration to suppress
cross-origin stylesheet security errors that occur when Sentry Replay
(rrweb) attempts to serialize DOM snapshots containing stylesheets from
browser extensions or CDN-loaded CSS. This prevents noise in Sentry
error logs without affecting the capture of legitimate errors.
</details>


<details><summary><h3>Confidence Score: 5/5</h3></summary>

- This PR is safe to merge with minimal risk
- The change adds a simple error filter to suppress benign cross-origin
stylesheet errors that are caused by Sentry Replay itself. The regex
pattern is specific and only affects client-side error reporting, with
no impact on application functionality or legitimate error capture
- No files require special attention
</details>


<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:37:54 +08:00
8 changed files with 268 additions and 5 deletions

View File

@@ -22,6 +22,11 @@ Sentry.init({
enabled: shouldEnable, enabled: shouldEnable,
// Suppress cross-origin stylesheet errors from Sentry Replay (rrweb)
// serializing DOM snapshots with cross-origin stylesheets
// (e.g., from browser extensions or CDN-loaded CSS)
ignoreErrors: [/Not allowed to access cross-origin stylesheet/],
// Add optional integrations for additional features // Add optional integrations for additional features
integrations: [ integrations: [
Sentry.captureConsoleIntegration(), Sentry.captureConsoleIntegration(),

View File

@@ -29,6 +29,7 @@ export function ScheduleListItem({
description={formatDistanceToNow(schedule.next_run_time, { description={formatDistanceToNow(schedule.next_run_time, {
addSuffix: true, addSuffix: true,
})} })}
descriptionTitle={new Date(schedule.next_run_time).toString()}
onClick={onClick} onClick={onClick}
selected={selected} selected={selected}
icon={ icon={

View File

@@ -7,6 +7,7 @@ import React from "react";
interface Props { interface Props {
title: string; title: string;
description?: string; description?: string;
descriptionTitle?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
selected?: boolean; selected?: boolean;
onClick?: () => void; onClick?: () => void;
@@ -16,6 +17,7 @@ interface Props {
export function SidebarItemCard({ export function SidebarItemCard({
title, title,
description, description,
descriptionTitle,
icon, icon,
selected, selected,
onClick, onClick,
@@ -38,7 +40,11 @@ export function SidebarItemCard({
> >
{title} {title}
</Text> </Text>
<Text variant="body" className="leading-tight !text-zinc-500"> <Text
variant="body"
className="leading-tight !text-zinc-500"
title={descriptionTitle}
>
{description} {description}
</Text> </Text>
</div> </div>

View File

@@ -81,6 +81,9 @@ export function TaskListItem({
? formatDistanceToNow(run.started_at, { addSuffix: true }) ? formatDistanceToNow(run.started_at, { addSuffix: true })
: "—" : "—"
} }
descriptionTitle={
run.started_at ? new Date(run.started_at).toString() : undefined
}
onClick={onClick} onClick={onClick}
selected={selected} selected={selected}
actions={ actions={

View File

@@ -180,3 +180,14 @@ body[data-google-picker-open="true"] [data-dialog-content] {
z-index: 1 !important; z-index: 1 !important;
pointer-events: none !important; pointer-events: none !important;
} }
/* CoPilot chat table styling — remove left/right borders, increase padding */
[data-streamdown="table-wrapper"] table {
border-left: none;
border-right: none;
}
[data-streamdown="table-wrapper"] th,
[data-streamdown="table-wrapper"] td {
padding: 0.875rem 1rem; /* py-3.5 px-4 */
}

View File

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

View File

@@ -226,7 +226,7 @@ function renderMarkdown(
table: ({ children, ...props }) => ( table: ({ children, ...props }) => (
<div className="my-4 overflow-x-auto"> <div className="my-4 overflow-x-auto">
<table <table
className="min-w-full divide-y divide-gray-200 rounded-lg border border-gray-200 dark:divide-gray-700 dark:border-gray-700" className="min-w-full divide-y divide-gray-200 border-y border-gray-200 dark:divide-gray-700 dark:border-gray-700"
{...props} {...props}
> >
{children} {children}
@@ -235,7 +235,7 @@ function renderMarkdown(
), ),
th: ({ children, ...props }) => ( th: ({ children, ...props }) => (
<th <th
className="bg-gray-50 px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-700 dark:bg-gray-800 dark:text-gray-300" className="bg-gray-50 px-4 py-3.5 text-left text-xs font-semibold uppercase tracking-wider text-gray-700 dark:bg-gray-800 dark:text-gray-300"
{...props} {...props}
> >
{children} {children}
@@ -243,7 +243,7 @@ function renderMarkdown(
), ),
td: ({ children, ...props }) => ( td: ({ children, ...props }) => (
<td <td
className="border-t border-gray-200 px-4 py-3 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-400" className="border-t border-gray-200 px-4 py-3.5 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-400"
{...props} {...props}
> >
{children} {children}

View File

@@ -0,0 +1,237 @@
/**
* Custom Streamdown code plugin with proper shiki singleton.
*
* Fixes SENTRY-1051: "@streamdown/code creates a new shiki highlighter per language,
* causing "10 instances created" warnings and memory bloat.
*
* This plugin creates ONE highlighter and loads languages dynamically.
*/
import {
createHighlighter,
bundledLanguages,
type Highlighter,
type BundledLanguage,
type BundledTheme,
} from "shiki";
// Types matching streamdown's expected interface
interface HighlightToken {
content: string;
color?: string;
bgColor?: string;
htmlStyle?: Record<string, string>;
htmlAttrs?: Record<string, string>;
offset?: number;
}
interface HighlightResult {
tokens: HighlightToken[][];
fg?: string;
bg?: string;
}
interface HighlightOptions {
code: string;
language: BundledLanguage;
themes: [string, string];
}
interface CodeHighlighterPlugin {
name: "shiki";
type: "code-highlighter";
highlight: (
options: HighlightOptions,
callback?: (result: HighlightResult) => void
) => HighlightResult | null;
supportsLanguage: (language: BundledLanguage) => boolean;
getSupportedLanguages: () => BundledLanguage[];
getThemes: () => [BundledTheme, BundledTheme];
}
// Singleton state
let highlighterPromise: Promise<Highlighter> | null = null;
let highlighterInstance: Highlighter | null = null;
const loadedLanguages = new Set<string>();
const pendingLanguages = new Map<string, Promise<void>>();
// Result cache (same as @streamdown/code)
const resultCache = new Map<string, HighlightResult>();
const pendingCallbacks = new Map<string, Set<(result: HighlightResult) => void>>();
// All supported languages
const supportedLanguages = new Set(Object.keys(bundledLanguages));
// Cache key for results
function getCacheKey(code: string, language: string, themes: [string, string]): string {
const prefix = code.slice(0, 100);
const suffix = code.length > 100 ? code.slice(-100) : "";
return `${language}:${themes[0]}:${themes[1]}:${code.length}:${prefix}:${suffix}`;
}
// Get or create the singleton highlighter
async function getHighlighter(themes: [string, string]): Promise<Highlighter> {
if (highlighterInstance) {
return highlighterInstance;
}
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: themes as [BundledTheme, BundledTheme],
// Start with common languages pre-loaded for faster first render
langs: ["javascript", "typescript", "python", "json", "html", "css", "bash", "markdown"],
}).then((h: Highlighter) => {
highlighterInstance = h;
["javascript", "typescript", "python", "json", "html", "css", "bash", "markdown"].forEach(
(l) => loadedLanguages.add(l)
);
return h;
});
}
return highlighterPromise;
}
// Load a language dynamically
async function ensureLanguageLoaded(
highlighter: Highlighter,
language: string
): Promise<void> {
if (loadedLanguages.has(language)) {
return;
}
if (pendingLanguages.has(language)) {
return pendingLanguages.get(language);
}
const loadPromise = highlighter
.loadLanguage(language as BundledLanguage)
.then(() => {
loadedLanguages.add(language);
pendingLanguages.delete(language);
})
.catch((err: Error) => {
console.warn(`[streamdown-code-singleton] Failed to load language: ${language}`, err);
pendingLanguages.delete(language);
});
pendingLanguages.set(language, loadPromise);
return loadPromise;
}
// Shiki token types
interface ShikiToken {
content: string;
color?: string;
htmlStyle?: Record<string, string>;
}
// Convert shiki tokens to streamdown format
function convertTokens(
shikiResult: ReturnType<Highlighter["codeToTokens"]>
): HighlightResult {
return {
tokens: shikiResult.tokens.map((line: ShikiToken[]) =>
line.map((token: ShikiToken) => ({
content: token.content,
color: token.color,
htmlStyle: token.htmlStyle,
}))
),
fg: shikiResult.fg,
bg: shikiResult.bg,
};
}
export interface CodePluginOptions {
themes?: [BundledTheme, BundledTheme];
}
export function createCodePlugin(
options: CodePluginOptions = {}
): CodeHighlighterPlugin {
const themes = options.themes ?? ["github-light", "github-dark"];
return {
name: "shiki",
type: "code-highlighter",
supportsLanguage(language: BundledLanguage): boolean {
return supportedLanguages.has(language);
},
getSupportedLanguages(): BundledLanguage[] {
return Array.from(supportedLanguages) as BundledLanguage[];
},
getThemes(): [BundledTheme, BundledTheme] {
return themes as [BundledTheme, BundledTheme];
},
highlight(
{ code, language, themes: highlightThemes }: HighlightOptions,
callback?: (result: HighlightResult) => void
): HighlightResult | null {
const cacheKey = getCacheKey(code, language, highlightThemes);
// Return cached result if available
if (resultCache.has(cacheKey)) {
return resultCache.get(cacheKey)!;
}
// Register callback for async result
if (callback) {
if (!pendingCallbacks.has(cacheKey)) {
pendingCallbacks.set(cacheKey, new Set());
}
pendingCallbacks.get(cacheKey)!.add(callback);
}
// Start async highlighting
getHighlighter(highlightThemes)
.then(async (highlighter) => {
// Ensure language is loaded
const lang = supportedLanguages.has(language) ? language : "text";
if (lang !== "text") {
await ensureLanguageLoaded(highlighter, lang);
}
// Highlight the code
const effectiveLang = highlighter.getLoadedLanguages().includes(lang)
? lang
: "text";
const shikiResult = highlighter.codeToTokens(code, {
lang: effectiveLang,
themes: {
light: highlightThemes[0] as BundledTheme,
dark: highlightThemes[1] as BundledTheme,
},
});
const result = convertTokens(shikiResult);
resultCache.set(cacheKey, result);
// Notify all pending callbacks
const callbacks = pendingCallbacks.get(cacheKey);
if (callbacks) {
for (const cb of callbacks) {
cb(result);
}
pendingCallbacks.delete(cacheKey);
}
})
.catch((err) => {
console.error("[streamdown-code-singleton] Failed to highlight code:", err);
pendingCallbacks.delete(cacheKey);
});
// Return null while async loading
return null;
},
};
}
// Pre-configured plugin with default settings (drop-in replacement for @streamdown/code)
export const code = createCodePlugin();