Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"debounce": "^2.1.1",
"diff": "^5.2.0",
"fast-deep-equal": "^3.1.3",
Expand All @@ -59,7 +58,6 @@
"react-compiler-runtime": "^1.0.0",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.1",
"react-icons": "^5.5.0",
"react-markdown": "^9.0.3",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.3",
Expand Down
8 changes: 4 additions & 4 deletions webview-ui/src/components/chat/Announcement.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { memo, type MouseEvent, type ReactNode, useState } from "react"
import { Trans } from "react-i18next"
import { SiDiscord, SiReddit, SiX } from "react-icons/si"

import { Package } from "@roo/package"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@src/components/ui"
import { DiscordIcon, RedditIcon, XIcon } from "@src/components/common/BrandIcons"
import { EXTERNAL_LINKS } from "@src/constants/externalLinks"

interface AnnouncementProps {
Expand Down Expand Up @@ -54,17 +54,17 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
<div className="mt-4 text-sm text-center text-vscode-descriptionForeground">
<div className="flex items-center justify-center gap-4">
<SocialLink
icon={<SiX className="w-4 h-4" aria-hidden />}
icon={<XIcon className="w-4 h-4" aria-hidden />}
label="X"
href="https://x.com/ZooCodeDev"
/>
<SocialLink
icon={<SiDiscord className="w-4 h-4" aria-hidden />}
icon={<DiscordIcon className="w-4 h-4" aria-hidden />}
label="Discord"
href="https://discord.gg/VxfP4Vx3gX"
/>
<SocialLink
icon={<SiReddit className="w-4 h-4" aria-hidden />}
icon={<RedditIcon className="w-4 h-4" aria-hidden />}
label="Reddit"
href="https://www.reddit.com/r/ZooCode/"
/>
Expand Down
40 changes: 40 additions & 0 deletions webview-ui/src/components/common/BrandIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { SVGProps } from "react"

type BrandIconProps = Omit<SVGProps<SVGSVGElement>, "viewBox" | "xmlns">

// Attribute order mirrors react-icons' `IconBase` so the serialized markup is
// byte-identical to what the library produced, which lets the spec assert exact
// equality instead of "looks the same".
const BrandIcon = ({ path, ...props }: BrandIconProps & { path: string }) => (

Check warning on line 8 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:8: Survived ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
role="img"
viewBox="0 0 24 24"
{...props}
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg">
<path d={path} />
</svg>
)

// Path data copied verbatim from react-icons 5.7.0 `react-icons/si`, which
// sources it from Simple Icons. Kept byte-identical so the marks render
// unchanged; do not hand-edit these strings.

const X_PATH =
"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"

Check warning on line 28 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:28: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.

const DISCORD_PATH =
"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"

Check warning on line 31 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:31: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.

const REDDIT_PATH =
"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"

Check warning on line 34 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:34: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.

export const XIcon = (props: BrandIconProps) => <BrandIcon path={X_PATH} {...props} />

Check warning on line 36 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:36: Survived ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.

export const DiscordIcon = (props: BrandIconProps) => <BrandIcon path={DISCORD_PATH} {...props} />

Check warning on line 38 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:38: Survived ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.

export const RedditIcon = (props: BrandIconProps) => <BrandIcon path={REDDIT_PATH} {...props} />

Check warning on line 40 in webview-ui/src/components/common/BrandIcons.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/common/BrandIcons.tsx:40: Survived ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.
42 changes: 42 additions & 0 deletions webview-ui/src/components/common/__tests__/BrandIcons.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react"
import { renderToStaticMarkup } from "react-dom/server"

import { DiscordIcon, RedditIcon, XIcon } from "../BrandIcons"

// Markup captured from react-icons 5.7.0 `react-icons/si` with the same props
// the Announcement passes, before the swap. Comparing the full serialized SVG
// proves the marks are byte-identical, not merely "looks similar".
const REACT_ICONS_MARKUP = {
X: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"></path></svg>',
Discord:
'<svg stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"></path></svg>',
Reddit: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"></path></svg>',
} as const

describe("BrandIcons", () => {
it.each([
["X", XIcon],
["Discord", DiscordIcon],
["Reddit", RedditIcon],
] as const)("renders %s byte-identically to react-icons", (name, Icon) => {
const markup = renderToStaticMarkup(<Icon className="w-4 h-4" aria-hidden />)

expect(markup).toBe(REACT_ICONS_MARKUP[name])
})

it("keeps the accessible name on the wrapping link, not the icon", () => {
// The icons are decorative: the visible label lives in a screen-reader-only
// span, so the SVG must stay out of the accessibility tree.
const markup = renderToStaticMarkup(<XIcon className="w-4 h-4" aria-hidden />)

expect(markup).toContain('aria-hidden="true"')
expect(markup).not.toContain("<title")
})

it("inherits color from the surrounding text", () => {
const markup = renderToStaticMarkup(<DiscordIcon />)

expect(markup).toContain('fill="currentColor"')
expect(markup).toContain('stroke="currentColor"')
})
})
84 changes: 82 additions & 2 deletions webview-ui/src/components/mcp/McpErrorRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,88 @@
import { useMemo } from "react"
import { formatRelative } from "date-fns"

import type { McpErrorEntry } from "@roo-code/types"

// `Intl.DateTimeFormat` instances are immutable and locale-fixed, so build them
// once per module instead of per render.
const TIME_FORMAT = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit" })

Check warning on line 7 in webview-ui/src/components/mcp/McpErrorRow.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/mcp/McpErrorRow.tsx:7: 4 mutation test gaps; example: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
const WEEKDAY_FORMAT = new Intl.DateTimeFormat("en-US", { weekday: "long" })

Check warning on line 8 in webview-ui/src/components/mcp/McpErrorRow.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/mcp/McpErrorRow.tsx:8: 3 mutation test gaps; example: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
const SHORT_DATE_FORMAT = new Intl.DateTimeFormat("en-US", {

Check warning on line 9 in webview-ui/src/components/mcp/McpErrorRow.tsx

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

webview-ui/src/components/mcp/McpErrorRow.tsx:9: 2 mutation test gaps; example: Survived StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
year: "numeric",
month: "2-digit",
day: "2-digit",
})

const MILLISECONDS_IN_DAY = 86_400_000

/**
* Whole calendar days from `baseDate` to `date`, in local time.
*
* Calendar days, not elapsed milliseconds: 23:59 and 00:01 on consecutive days
* are one day apart even though only two minutes elapsed. Local midnights are
* compared directly, so DST shifts (23h or 25h days) still round to one day.
*
* The inputs are copied and zeroed with `setHours` rather than rebuilt through
* `new Date(year, month, day)`, because that constructor maps years 0-99 onto
* 1900-1999: a date in year 99 would be silently read as 1999 and produce a
* delta of centuries instead of a day.
*/
const differenceInCalendarDays = (date: Date, baseDate: Date): number => {
const dateMidnight = new Date(date)
dateMidnight.setHours(0, 0, 0, 0)

const baseMidnight = new Date(baseDate)
baseMidnight.setHours(0, 0, 0, 0)

return Math.round((dateMidnight.getTime() - baseMidnight.getTime()) / MILLISECONDS_IN_DAY)
}

/**
* Render a timestamp the way `date-fns`' `formatRelative` did for the `en-US`
* locale: a calendar phrase for the surrounding two weeks, a short date beyond.
*
* | Distance to `baseDate` | Result |
* | ---------------------- | ----------------------- |
* | Same day | `today at 9:00 AM` |
* | Previous day | `yesterday at 9:00 AM` |
* | Next day | `tomorrow at 9:00 AM` |
* | Previous 2-6 days | `last Tuesday at 9:00 AM` |
* | Next 2-6 days | `Saturday at 9:00 AM` |
* | Anything else | `10/22/2026` |
*
* The output is deliberately fixed to `en-US` because that is what the previous
* implementation produced: `formatRelative` was called without a `locale`
* option, so it used date-fns' `enUS` default regardless of the UI language.
*
* @throws {RangeError} When `date` is an invalid date.
*/
export const formatRelativeTimestamp = (date: Date, baseDate: Date): string => {
const dayDelta = differenceInCalendarDays(date, baseDate)

if (Number.isNaN(dayDelta)) {
throw new RangeError("Invalid time value")
}

const time = TIME_FORMAT.format(date)

if (dayDelta < -6 || dayDelta >= 7) {
return SHORT_DATE_FORMAT.format(date)
}
if (dayDelta < -1) {
return `last ${WEEKDAY_FORMAT.format(date)} at ${time}`
}
if (dayDelta < 0) {
return `yesterday at ${time}`
}
if (dayDelta < 1) {
return `today at ${time}`
}
if (dayDelta < 2) {
return `tomorrow at ${time}`
}

return `${WEEKDAY_FORMAT.format(date)} at ${time}`
}

type McpErrorRowProps = {
error: McpErrorEntry
}
Expand All @@ -25,7 +105,7 @@
{error.message}
</div>
<div className="text-xs text-vscode-descriptionForeground">
{formatRelative(error.timestamp, new Date())}
{formatRelativeTimestamp(new Date(error.timestamp), new Date())}
</div>
</div>
)
Expand Down
Loading
Loading