diff --git a/apps/web/src/components/terminal/TerminalPanel.tsx b/apps/web/src/components/terminal/TerminalPanel.tsx
new file mode 100644
index 0000000..3f0ac80
--- /dev/null
+++ b/apps/web/src/components/terminal/TerminalPanel.tsx
@@ -0,0 +1,257 @@
+import type { ReactElement, CSSProperties } from "react";
+
+export interface TerminalLine {
+ type: "prompt" | "command" | "output" | "error" | "warning" | "success";
+ content: string;
+}
+
+export interface TerminalTab {
+ id: string;
+ label: string;
+}
+
+export interface TerminalPanelProps {
+ open: boolean;
+ onClose: () => void;
+ tabs?: TerminalTab[];
+ activeTab?: string;
+ onTabChange?: (id: string) => void;
+ lines?: TerminalLine[];
+ className?: string;
+}
+
+const defaultTabs: TerminalTab[] = [
+ { id: "main", label: "main" },
+ { id: "build", label: "build" },
+ { id: "logs", label: "logs" },
+];
+
+const blinkKeyframes = `
+@keyframes ms-terminal-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+`;
+
+let blinkStyleInjected = false;
+
+function ensureBlinkStyle(): void {
+ if (blinkStyleInjected || typeof document === "undefined") return;
+ const styleEl = document.createElement("style");
+ styleEl.textContent = blinkKeyframes;
+ document.head.appendChild(styleEl);
+ blinkStyleInjected = true;
+}
+
+function getLineColor(type: TerminalLine["type"]): string {
+ switch (type) {
+ case "prompt":
+ return "var(--success)";
+ case "command":
+ return "var(--text-2)";
+ case "output":
+ return "var(--muted)";
+ case "error":
+ return "var(--danger)";
+ case "warning":
+ return "var(--warn)";
+ case "success":
+ return "var(--success)";
+ default:
+ return "var(--muted)";
+ }
+}
+
+export function TerminalPanel({
+ open,
+ onClose,
+ tabs,
+ activeTab,
+ onTabChange,
+ lines = [],
+ className = "",
+}: TerminalPanelProps): ReactElement {
+ ensureBlinkStyle();
+
+ const resolvedTabs = tabs ?? defaultTabs;
+ const resolvedActiveTab = activeTab ?? resolvedTabs[0]?.id ?? "";
+
+ const panelStyle: CSSProperties = {
+ background: "var(--bg-deep)",
+ borderTop: "1px solid var(--border)",
+ overflow: "hidden",
+ flexShrink: 0,
+ display: "flex",
+ flexDirection: "column",
+ height: open ? 280 : 0,
+ transition: "height 0.3s ease",
+ };
+
+ const headerStyle: CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: 10,
+ padding: "6px 16px",
+ borderBottom: "1px solid var(--border)",
+ flexShrink: 0,
+ };
+
+ const tabBarStyle: CSSProperties = {
+ display: "flex",
+ gap: 2,
+ };
+
+ const actionsStyle: CSSProperties = {
+ marginLeft: "auto",
+ display: "flex",
+ gap: 4,
+ };
+
+ const bodyStyle: CSSProperties = {
+ flex: 1,
+ overflowY: "auto",
+ padding: "10px 16px",
+ fontFamily: "var(--mono)",
+ fontSize: "0.78rem",
+ lineHeight: 1.6,
+ };
+
+ const cursorStyle: CSSProperties = {
+ display: "inline-block",
+ width: 7,
+ height: 14,
+ background: "var(--success)",
+ marginLeft: 2,
+ animation: "ms-terminal-blink 1s step-end infinite",
+ verticalAlign: "text-bottom",
+ };
+
+ return (
+
+ {/* Header */}
+
+ {/* Tab bar */}
+
+ {resolvedTabs.map((tab) => {
+ const isActive = tab.id === resolvedActiveTab;
+ const tabStyle: CSSProperties = {
+ padding: "3px 10px",
+ borderRadius: 4,
+ fontSize: "0.75rem",
+ fontFamily: "var(--mono)",
+ color: isActive ? "var(--success)" : "var(--muted)",
+ cursor: "pointer",
+ background: isActive ? "var(--surface)" : "transparent",
+ border: "none",
+ outline: "none",
+ };
+
+ return (
+
+ );
+ })}
+
+
+ {/* Action buttons */}
+
+
+
+
+
+ {/* Body */}
+
+ {lines.map((line, index) => {
+ const isLast = index === lines.length - 1;
+ const lineStyle: CSSProperties = {
+ display: "flex",
+ gap: 8,
+ };
+ const contentStyle: CSSProperties = {
+ color: getLineColor(line.type),
+ };
+
+ return (
+
+
+ {line.content}
+ {isLast && }
+
+
+ );
+ })}
+
+ {/* Show cursor even when no lines */}
+ {lines.length === 0 && (
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/components/terminal/index.ts b/apps/web/src/components/terminal/index.ts
new file mode 100644
index 0000000..cd385a0
--- /dev/null
+++ b/apps/web/src/components/terminal/index.ts
@@ -0,0 +1,2 @@
+export type { TerminalLine, TerminalTab, TerminalPanelProps } from "./TerminalPanel";
+export { TerminalPanel } from "./TerminalPanel";
diff --git a/docs/tasks.md b/docs/tasks.md
index 9852f54..00643be 100644
--- a/docs/tasks.md
+++ b/docs/tasks.md
@@ -16,11 +16,11 @@
| MS15-FE-004 | done | Topbar/Header component (logo, search, status, notifications, theme toggle, avatar dropdown) | #448 | web | feat/ms15-design-system | MS15-FE-002 | MS15-FE-005 | w-4 | 2026-02-22T15:30Z | 2026-02-22T16:00Z | 25K | 44K | Search, status, notifications, avatar dropdown. Commit 04f9918. |
| MS15-FE-005 | done | Responsive layout (breakpoints, hamburger, sidebar auto-hide at mobile) | #448 | web | feat/ms15-design-system | MS15-FE-003,MS15-FE-004 | MS15-QA-001 | w-5 | 2026-02-22T16:00Z | 2026-02-22T16:30Z | 20K | 57K | Mobile overlay, hamburger, matchMedia. Commit 28620b2. |
| MS15-FE-006 | done | Loading spinner (Mosaic logo icon with rotation animation, site-wide) | #448 | web | feat/ms15-design-system | MS15-FE-001 | | w-2 | 2026-02-22T14:30Z | 2026-02-22T15:00Z | 10K | 8K | MosaicLogo + MosaicSpinner components. Same commit e615fa8. |
-| MS15-UI-001 | not-started | Align packages/ui tokens with new CSS variable design system | #449 | ui | feat/ms15-shared-components | MS15-FE-001 | MS15-UI-002,MS15-UI-003,MS15-UI-004 | | | | 20K | | |
-| MS15-UI-002 | not-started | Update Card, Badge, Button, Dot component variants to match reference | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-001 | | | | 25K | | |
-| MS15-UI-003 | not-started | Create MetricsStrip, ProgressBar, FilterTabs shared components | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-001 | | | | 20K | | |
-| MS15-UI-004 | not-started | Create SectionHeader, Table, LogLine shared components | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-002 | | | | 15K | | |
-| MS15-UI-005 | not-started | Create Terminal panel component (bottom drawer, tabs, output) | #449 | web | feat/ms15-shared-components | MS15-UI-001 | | | | | 20K | | |
+| MS15-UI-001 | done | Align packages/ui tokens with new CSS variable design system | #449 | ui | feat/ms15-shared-components | MS15-FE-001 | MS15-UI-002,MS15-UI-003,MS15-UI-004 | w-6 | 2026-02-22T17:00Z | 2026-02-22T17:30Z | 20K | 35K | Combined with UI-002. Commit 44011f4. Build passes. |
+| MS15-UI-002 | done | Update Card, Badge, Button, Dot component variants to match reference | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-001 | w-6 | 2026-02-22T17:00Z | 2026-02-22T17:30Z | 25K | 0K | Combined with UI-001 (same commit 44011f4). |
+| MS15-UI-003 | done | Create MetricsStrip, ProgressBar, FilterTabs shared components | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-001 | w-7 | 2026-02-22T17:30Z | 2026-02-22T18:00Z | 20K | 30K | Commit 9b0445c. Build passes. |
+| MS15-UI-004 | done | Create SectionHeader, Table, LogLine shared components | #449 | ui | feat/ms15-shared-components | MS15-UI-001 | MS15-DASH-002 | w-8 | 2026-02-22T17:30Z | 2026-02-22T18:00Z | 15K | 25K | Commit 9b0445c. Build passes. |
+| MS15-UI-005 | done | Create Terminal panel component (bottom drawer, tabs, output) | #449 | web | feat/ms15-shared-components | MS15-UI-001 | | w-9 | 2026-02-22T17:30Z | 2026-02-22T18:00Z | 20K | 30K | Commit 9b0445c. Build passes. |
| MS15-DASH-001 | not-started | Dashboard metrics strip (6 cells, colored borders, values, trends) | #450 | web | feat/ms15-dashboard-page | MS15-UI-002,MS15-UI-003 | | | | | 15K | | |
| MS15-DASH-002 | not-started | Active Orchestrator Sessions card with agent nodes | #450 | web | feat/ms15-dashboard-page | MS15-UI-004 | | | | | 20K | | |
| MS15-DASH-003 | not-started | Quick Actions 2x2 grid | #450 | web | feat/ms15-dashboard-page | MS15-UI-002 | | | | | 10K | | |
diff --git a/packages/ui/src/components/Avatar.tsx b/packages/ui/src/components/Avatar.tsx
index a6c0743..a0c02d8 100644
--- a/packages/ui/src/components/Avatar.tsx
+++ b/packages/ui/src/components/Avatar.tsx
@@ -15,41 +15,51 @@ export function Avatar({
fallback,
initials,
className = "",
+ style,
...props
}: AvatarProps): ReactElement {
- const sizeStyles = {
+ type AvatarSize = "sm" | "md" | "lg" | "xl";
+ const sizeStyles: Record = {
sm: "w-6 h-6 text-xs",
md: "w-8 h-8 text-sm",
lg: "w-12 h-12 text-base",
xl: "w-16 h-16 text-xl",
};
- const baseStyles =
- "rounded-full overflow-hidden flex items-center justify-center bg-gray-200 font-medium text-gray-600";
+ const baseClass = `rounded-full overflow-hidden flex items-center justify-center font-medium ${sizeStyles[size]} ${className}`;
- const combinedClassName = [baseStyles, sizeStyles[size], className].filter(Boolean).join(" ");
+ const gradientStyle: React.CSSProperties = {
+ background: "linear-gradient(135deg, var(--ms-blue-500), var(--ms-purple-500))",
+ color: "#fff",
+ ...style,
+ };
if (src) {
- return
;
+ return (
+
+ );
}
if (fallback) {
- return {fallback}
;
+ return (
+
+ {fallback}
+
+ );
}
if (initials) {
- return {initials}
;
+ return (
+
+ {initials}
+
+ );
}
// Default fallback with user icon
return (
-
-