diff --git a/src/components/TrayPopover.tsx b/src/components/TrayPopover.tsx
new file mode 100644
index 0000000..dba3d63
--- /dev/null
+++ b/src/components/TrayPopover.tsx
@@ -0,0 +1,150 @@
+/**
+ * TrayPopover -- compact view of THE ONE THING for system tray.
+ *
+ * Shows:
+ * - Current focus item with quick actions (Start/Defer/Skip)
+ * - Queue and inbox counts
+ * - Link to open full window
+ */
+
+import type { FocusItem, DeferDuration } from "@/lib/types";
+
+interface TrayPopoverProps {
+ /** Current focus item, or null if nothing is focused */
+ focusItem: FocusItem | null;
+ /** Number of items in the queue */
+ queueCount: number;
+ /** Number of items in the inbox */
+ inboxCount: number;
+ /** Called when Start is clicked */
+ onStart?: () => void;
+ /** Called when Defer is clicked, with duration */
+ onDefer?: (duration: DeferDuration) => void;
+ /** Called when Skip is clicked */
+ onSkip?: () => void;
+ /** Called when "Full window" is clicked */
+ onOpenFull?: () => void;
+}
+
+/**
+ * Format a relative time string like "2d" for "2 days ago"
+ */
+function formatAge(isoDate: string | null): string {
+ if (!isoDate) return "";
+
+ const now = Date.now();
+ const then = new Date(isoDate).getTime();
+ const diff = now - then;
+
+ if (Number.isNaN(diff)) return "";
+
+ const minutes = Math.floor(diff / (1000 * 60));
+ if (minutes < 60) return `${minutes}m`;
+
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h`;
+
+ const days = Math.floor(hours / 24);
+ return `${days}d`;
+}
+
+/**
+ * Format the type label for display
+ */
+function formatTypeLabel(type: FocusItem["type"]): string {
+ switch (type) {
+ case "mr_review":
+ return "Review";
+ case "mr_authored":
+ return "MR";
+ case "issue":
+ return "Issue";
+ case "manual":
+ return "Task";
+ }
+}
+
+export function TrayPopover({
+ focusItem,
+ queueCount,
+ inboxCount,
+ onStart,
+ onDefer,
+ onSkip,
+ onOpenFull,
+}: TrayPopoverProps): React.ReactElement {
+ return (
+
+ {/* Focus Item Section */}
+
+ {focusItem ? (
+ <>
+
+ THE ONE THING
+
+
+ {focusItem.title}
+
+
+
+ {formatTypeLabel(focusItem.type)}
+
+ {focusItem.project}
+ {focusItem.updatedAt && (
+ <>
+ ยท
+ {formatAge(focusItem.updatedAt)}
+ >
+ )}
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+ >
+ ) : (
+
+ Nothing focused. Pick something from the queue!
+
+ )}
+
+
+ {/* Separator */}
+
+
+ {/* Counts Row */}
+
+ Queue: {queueCount}
+ Inbox: {inboxCount}
+
+
+ {/* Full Window Link */}
+
+
+
+
+ );
+}
diff --git a/tests/components/TrayPopover.test.tsx b/tests/components/TrayPopover.test.tsx
new file mode 100644
index 0000000..f05f955
--- /dev/null
+++ b/tests/components/TrayPopover.test.tsx
@@ -0,0 +1,198 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { TrayPopover } from "@/components/TrayPopover";
+import { makeFocusItem } from "../helpers/fixtures";
+
+describe("TrayPopover", () => {
+ describe("with focus item", () => {
+ it("shows 'THE ONE THING' header", () => {
+ const item = makeFocusItem({ title: "Review MR !847" });
+ render();
+
+ expect(screen.getByText("THE ONE THING")).toBeInTheDocument();
+ });
+
+ it("shows focus item title", () => {
+ const item = makeFocusItem({ title: "Fix auth bug" });
+ render();
+
+ expect(screen.getByText("Fix auth bug")).toBeInTheDocument();
+ });
+
+ it("shows project path", () => {
+ const item = makeFocusItem({ project: "platform/core" });
+ render();
+
+ expect(screen.getByText(/platform\/core/)).toBeInTheDocument();
+ });
+
+ it("shows type badge", () => {
+ const item = makeFocusItem({ type: "mr_review" });
+ render();
+
+ expect(screen.getByText(/review/i)).toBeInTheDocument();
+ });
+
+ it("shows Start button", () => {
+ const item = makeFocusItem();
+ render();
+
+ expect(screen.getByRole("button", { name: /start/i })).toBeInTheDocument();
+ });
+
+ it("shows Defer button", () => {
+ const item = makeFocusItem();
+ render();
+
+ expect(screen.getByRole("button", { name: /defer/i })).toBeInTheDocument();
+ });
+
+ it("shows Skip button", () => {
+ const item = makeFocusItem();
+ render();
+
+ expect(screen.getByRole("button", { name: /skip/i })).toBeInTheDocument();
+ });
+ });
+
+ describe("empty state", () => {
+ it("shows empty message when no focus item", () => {
+ render();
+
+ expect(screen.getByText(/nothing focused/i)).toBeInTheDocument();
+ });
+
+ it("does not show action buttons when empty", () => {
+ render();
+
+ expect(screen.queryByRole("button", { name: /start/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /defer/i })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /skip/i })).not.toBeInTheDocument();
+ });
+ });
+
+ describe("counts", () => {
+ it("shows queue count", () => {
+ render();
+
+ expect(screen.getByText("Queue: 4")).toBeInTheDocument();
+ });
+
+ it("shows inbox count", () => {
+ render();
+
+ expect(screen.getByText("Inbox: 3")).toBeInTheDocument();
+ });
+
+ it("shows both counts", () => {
+ render();
+
+ expect(screen.getByText("Queue: 5")).toBeInTheDocument();
+ expect(screen.getByText("Inbox: 2")).toBeInTheDocument();
+ });
+ });
+
+ describe("full window link", () => {
+ it("shows keyboard shortcut hint", () => {
+ render();
+
+ // Should show some reference to opening full window
+ expect(screen.getByText(/full window/i)).toBeInTheDocument();
+ });
+ });
+
+ describe("actions", () => {
+ it("calls onStart when Start clicked", async () => {
+ const user = userEvent.setup();
+ const onStart = vi.fn();
+ const item = makeFocusItem();
+
+ render(
+
+ );
+
+ await user.click(screen.getByRole("button", { name: /start/i }));
+ expect(onStart).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onDefer with duration when Defer clicked", async () => {
+ const user = userEvent.setup();
+ const onDefer = vi.fn();
+ const item = makeFocusItem();
+
+ render(
+
+ );
+
+ await user.click(screen.getByRole("button", { name: /defer/i }));
+ expect(onDefer).toHaveBeenCalledWith("1h");
+ });
+
+ it("calls onSkip when Skip clicked", async () => {
+ const user = userEvent.setup();
+ const onSkip = vi.fn();
+ const item = makeFocusItem();
+
+ render(
+
+ );
+
+ await user.click(screen.getByRole("button", { name: /skip/i }));
+ expect(onSkip).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onOpenFull when full window link clicked", async () => {
+ const user = userEvent.setup();
+ const onOpenFull = vi.fn();
+
+ render(
+
+ );
+
+ await user.click(screen.getByText(/full window/i));
+ expect(onOpenFull).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("relative time", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-02-26T12:00:00Z"));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("shows age for items", () => {
+ const item = makeFocusItem({
+ updatedAt: new Date("2026-02-24T12:00:00Z").toISOString(), // 2 days ago
+ });
+
+ render();
+
+ expect(screen.getByText(/2d/)).toBeInTheDocument();
+ });
+ });
+});