diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx
index b86d06120d..6b3679bb37 100644
--- a/frontend/app/tab/tab.tsx
+++ b/frontend/app/tab/tab.tsx
@@ -1,18 +1,18 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
-import { getTabBadgeAtom, sortBadgesForTab } from "@/app/store/badge";
+import { getTabBadgeAtom } from "@/app/store/badge";
import { getOrefMetaKeyAtom, globalStore, recordTEvent, refocusNode } from "@/app/store/global";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { WaveEnv, WaveEnvSubset, useWaveEnv } from "@/app/waveenv/waveenv";
import { Button } from "@/element/button";
import { validateCssColor } from "@/util/color-validator";
-import { fireAndForget, makeIconClass } from "@/util/util";
+import { fireAndForget } from "@/util/util";
import clsx from "clsx";
import { useAtomValue } from "jotai";
-import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
-import { v7 as uuidv7 } from "uuid";
+import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
import { makeORef } from "../store/wos";
+import { TabBadges } from "./tabbadges";
import "./tab.scss";
type TabEnv = WaveEnvSubset<{
@@ -47,47 +47,6 @@ interface TabVProps {
renameRef?: React.RefObject<(() => void) | null>;
}
-interface TabBadgesProps {
- badges?: Badge[] | null;
- flagColor?: string | null;
-}
-
-function TabBadges({ badges, flagColor }: TabBadgesProps) {
- const flagBadgeId = useMemo(() => uuidv7(), []);
- const allBadges = useMemo(() => {
- const base = badges ?? [];
- if (!flagColor) {
- return base;
- }
- const flagBadge: Badge = { icon: "flag", color: flagColor, priority: 0, badgeid: flagBadgeId };
- return sortBadgesForTab([...base, flagBadge]);
- }, [badges, flagColor, flagBadgeId]);
- if (!allBadges[0]) {
- return null;
- }
- const firstBadge = allBadges[0];
- const extraBadges = allBadges.slice(1, 3);
- return (
-
-
- {extraBadges.length > 0 && (
-
- {extraBadges.map((badge, idx) => (
-
- ))}
-
- )}
-
- );
-}
-
const TabV = forwardRef((props, ref) => {
const {
tabId,
diff --git a/frontend/app/tab/tabbadges.tsx b/frontend/app/tab/tabbadges.tsx
new file mode 100644
index 0000000000..0e56cc89af
--- /dev/null
+++ b/frontend/app/tab/tabbadges.tsx
@@ -0,0 +1,52 @@
+// Copyright 2026, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { sortBadgesForTab } from "@/app/store/badge";
+import { cn, makeIconClass } from "@/util/util";
+import { useMemo } from "react";
+import { v7 as uuidv7 } from "uuid";
+
+export interface TabBadgesProps {
+ badges?: Badge[] | null;
+ flagColor?: string | null;
+ className?: string;
+}
+
+const DefaultClassName =
+ "pointer-events-none absolute left-[4px] top-1/2 z-[3] flex h-[20px] w-[20px] -translate-y-1/2 items-center justify-center px-[2px] py-[1px]";
+
+export function TabBadges({ badges, flagColor, className }: TabBadgesProps) {
+ const flagBadgeId = useMemo(() => uuidv7(), []);
+ const allBadges = useMemo(() => {
+ const base = badges ?? [];
+ if (!flagColor) {
+ return base;
+ }
+ const flagBadge: Badge = { icon: "flag", color: flagColor, priority: 0, badgeid: flagBadgeId };
+ return sortBadgesForTab([...base, flagBadge]);
+ }, [badges, flagColor, flagBadgeId]);
+ if (!allBadges[0]) {
+ return null;
+ }
+ const firstBadge = allBadges[0];
+ const extraBadges = allBadges.slice(1, 3);
+ return (
+
+
+ {extraBadges.length > 0 && (
+
+ {extraBadges.map((badge, idx) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/frontend/app/tab/vtab.test.tsx b/frontend/app/tab/vtab.test.tsx
new file mode 100644
index 0000000000..b995b6a72a
--- /dev/null
+++ b/frontend/app/tab/vtab.test.tsx
@@ -0,0 +1,63 @@
+// Copyright 2026, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { renderToStaticMarkup } from "react-dom/server";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { VTab, VTabItem } from "./vtab";
+
+const OriginalCss = globalThis.CSS;
+const HexColorRegex = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i;
+
+function renderVTab(tab: VTabItem): string {
+ return renderToStaticMarkup(
+ null}
+ onDragStart={() => null}
+ onDragOver={() => null}
+ onDrop={() => null}
+ onDragEnd={() => null}
+ />
+ );
+}
+
+describe("VTab badges", () => {
+ beforeAll(() => {
+ globalThis.CSS = {
+ supports: (_property: string, value: string) => HexColorRegex.test(value),
+ } as typeof CSS;
+ });
+
+ afterAll(() => {
+ globalThis.CSS = OriginalCss;
+ });
+
+ it("renders shared badges and a validated flag badge", () => {
+ const markup = renderVTab({
+ id: "tab-1",
+ name: "Build Logs",
+ badges: [{ badgeid: "badge-1", icon: "bell", color: "#f59e0b", priority: 2 }],
+ flagColor: "#429DFF",
+ });
+
+ expect(markup).toContain("#429DFF");
+ expect(markup).toContain("#f59e0b");
+ expect(markup).toContain("rounded-full");
+ });
+
+ it("ignores invalid flag colors", () => {
+ const markup = renderVTab({
+ id: "tab-2",
+ name: "Deploy",
+ badges: [{ badgeid: "badge-2", icon: "bell", color: "#4ade80", priority: 2 }],
+ flagColor: "definitely-not-a-color",
+ });
+
+ expect(markup).not.toContain("definitely-not-a-color");
+ expect(markup).not.toContain("fa-flag");
+ expect(markup).toContain("#4ade80");
+ });
+});
diff --git a/frontend/app/tab/vtab.tsx b/frontend/app/tab/vtab.tsx
index e5689de8bb..b6c3a29a54 100644
--- a/frontend/app/tab/vtab.tsx
+++ b/frontend/app/tab/vtab.tsx
@@ -1,9 +1,10 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
-import { makeIconClass } from "@/util/util";
+import { validateCssColor } from "@/util/color-validator";
import { cn } from "@/util/util";
import { useCallback, useEffect, useRef, useState } from "react";
+import { TabBadges } from "./tabbadges";
const RenameFocusDelayMs = 50;
@@ -11,6 +12,8 @@ export interface VTabItem {
id: string;
name: string;
badge?: Badge | null;
+ badges?: Badge[] | null;
+ flagColor?: string | null;
}
interface VTabProps {
@@ -44,6 +47,18 @@ export function VTab({
const [isEditable, setIsEditable] = useState(false);
const editableRef = useRef(null);
const editableTimeoutRef = useRef(null);
+ const badges = tab.badges ?? (tab.badge ? [tab.badge] : null);
+
+ const rawFlagColor = tab.flagColor;
+ let flagColor: string | null = null;
+ if (rawFlagColor) {
+ try {
+ validateCssColor(rawFlagColor);
+ flagColor = rawFlagColor;
+ } catch {
+ flagColor = null;
+ }
+ }
useEffect(() => {
setOriginalName(tab.name);
@@ -139,11 +154,11 @@ export function VTab({
isDragging && "opacity-50"
)}
>
- {tab.badge && (
-
-
-
- )}
+