feat(tg): Mini App V6 — premium overhaul (#609)

* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs)

Design system: native type with tabular-numeral heroes (mono demoted to
the wordmark), borderless elevated cards, floating dock, Telegram
window-chrome painting via the theme bridge; Inbox moves behind a header
bell with humanized notifications (UUIDs resolve to task names).

Chat: honest Mine/Fleet split — participant-scoped CEO threads with real
unread counts and mark-read, watched fleet threads with reply-as-CEO on
task-linked conversations (watch-only otherwise), markdown transcripts,
live pulse flashes, and a pinned Secretary live chat on the panel's SSE
session runtime.

Metrics: new tab with period-segmented spend hero, by-agent/team/model
breakdowns, delivery + efficiency health, and a per-agent drilldown over
usage time-series (agent_slug) + member scorecard.

Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper;
task sheet gains the CEO decide verbs (approve / request changes /
unblock).

Security: /api/dashboard router now require_panel_token-gated at router
level (mirrors /api/usage), closing unauthenticated metrics exposure.

* fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars

The mono returns as the numeral/brand voice (.tg-display — heroes, stat
values, wordmark) while labels stay native sentence case. The hand-drawn
duotone glyphs and lucide feature icons are replaced by Phosphor (MIT):
duotone at rest via an IconContext at the shell, filled weight on the
dock's active tab; row glyph maps (board statuses, inbox kinds, approval
kinds, quick actions) all move over. Team avatar tiles drop their borders
— tint-only squircles.

* fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap

* polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing

Applied the design-taste audit against the cockpit: every em-dash in
visible UI copy rewritten (periods/commas/colons), the remaining lucide
chrome (carets, arrows, send, close, spinners) moved to Phosphor so the
tg tree ships one icon family (send is the native paper-plane, carets
bold), the hand-rolled chevron SVG deleted, and metadata lines rationed
to a single middle-dot separator.

* polish(tg): pipeline chip strip scrolls without a visible scrollbar

* fix(tests): metrics observability fixture uses a relative timestamp

The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window
exactly 30 days later, detonating the suite on every branch. Two days
back from now() stays inside every window (30d metrics, 7d scorecards)
permanently.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-20 17:29:22 +02:00
committed by GitHub
co-authored by Renn F
parent cd73ad6a74
commit 3c5ee46347
41 changed files with 4227 additions and 812 deletions
@@ -131,7 +131,7 @@ describe("TgApprovalsTab", () => {
renderTab();
await userEvent.click(await screen.findByText(/^x+$/));
expect(screen.getByText("281/280")).toBeInTheDocument();
expect(screen.getByText("281 / 280")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /post to x/i })).toBeDisabled();
});
@@ -166,7 +166,9 @@ describe("TgApprovalsTab", () => {
renderTab();
await userEvent.click(await screen.findByText("Shipped a thing."));
expect(screen.getByRole("button", { name: /post to x/i })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /post to x/i }),
).toBeInTheDocument();
// Outside Telegram there's no native BackButton — the visible fallback
// arrow renders instead.
@@ -0,0 +1,81 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TgBoardTab } from "../tg-board-tab";
import { TaskStatus, Team } from "@/types";
const { tasks } = vi.hoisted(() => ({
tasks: { current: [] as Array<Record<string, unknown>> },
}));
vi.mock("@/hooks/use-tasks", () => ({
useTasks: () => ({ data: tasks.current, isLoading: false }),
}));
vi.mock("@/components/tg/tg-task-sheet", () => ({
TgTaskSheet: () => null,
}));
function task(overrides: Record<string, unknown>) {
return {
team: Team.BACKEND,
assigned_to: null,
updated_at: "2026-07-19T00:00:00Z",
...overrides,
};
}
describe("TgBoardTab", () => {
it("groups tasks by lifecycle stage and collapses done by default", async () => {
tasks.current = [
task({ id: "t1", title: "Blocked task", status: TaskStatus.BLOCKED }),
task({ id: "t2", title: "QA task", status: TaskStatus.AWAITING_QA }),
task({ id: "t3", title: "Flight task", status: TaskStatus.IN_PROGRESS }),
task({ id: "t4", title: "Queued task", status: TaskStatus.PENDING }),
task({
id: "t5",
title: "Done task A",
status: TaskStatus.COMPLETED,
updated_at: "2026-07-19T02:00:00Z",
}),
task({
id: "t6",
title: "Done task B",
status: TaskStatus.COMPLETED,
updated_at: "2026-07-19T01:00:00Z",
}),
task({
id: "t7",
title: "Cancelled task",
status: TaskStatus.CANCELLED,
updated_at: "2026-07-19T00:30:00Z",
}),
];
render(<TgBoardTab />);
// Every non-empty group renders its section + its task.
expect(screen.getByText("Needs you")).toBeInTheDocument();
expect(screen.getByText("Blocked task")).toBeInTheDocument();
expect(screen.getByText("In review")).toBeInTheDocument();
expect(screen.getByText("QA task")).toBeInTheDocument();
expect(screen.getByText("In flight")).toBeInTheDocument();
expect(screen.getByText("Flight task")).toBeInTheDocument();
expect(screen.getByText("Queued")).toBeInTheDocument();
expect(screen.getByText("Queued task")).toBeInTheDocument();
// Done is collapsed to a tally — no task titles rendered yet.
expect(screen.getByText("Done")).toBeInTheDocument();
expect(screen.getByText(/2 completed · 1 cancelled/)).toBeInTheDocument();
expect(screen.queryByText("Done task A")).not.toBeInTheDocument();
// Expanding reveals the recent terminal tasks.
await userEvent.click(screen.getByText(/2 completed · 1 cancelled/));
expect(screen.getByText("Done task A")).toBeInTheDocument();
expect(screen.getByText("Cancelled task")).toBeInTheDocument();
});
it("shows a friendly empty state with no tasks", () => {
tasks.current = [];
render(<TgBoardTab />);
expect(screen.getByText(/no tasks yet/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,214 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TgChatTab } from "../tg-chat-tab";
const { mineItems, fleetItems, messages, sendMock, replyMock, markReadMock } =
vi.hoisted(() => ({
mineItems: { current: [] as Array<Record<string, unknown>> },
fleetItems: { current: [] as Array<Record<string, unknown>> },
messages: { current: [] as Array<Record<string, unknown>> },
sendMock: vi.fn(),
replyMock: vi.fn(),
markReadMock: vi.fn(),
}));
vi.mock("@/hooks/use-a2a-live", () => ({
a2aLiveKeys: {
all: ["a2a-live"],
conversations: ["a2a-live", "conversations"],
ceoConversations: ["a2a-live", "ceo-conversations"],
pairs: ["a2a-live", "pairs"],
messages: (id: string) => ["a2a-live", "messages", id],
},
useCeoConversations: () => ({
data: { items: mineItems.current, total: mineItems.current.length },
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useA2AConversations: () => ({
data: { items: fleetItems.current, total: fleetItems.current.length },
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useA2AMessages: () => ({
data: {
items: messages.current,
total: messages.current.length,
has_more: false,
},
isLoading: false,
}),
useSendCeoMessage: () => ({ mutate: sendMock, isPending: false }),
useReplyAsCeo: () => ({ mutate: replyMock, isPending: false }),
useCreateCeoConversation: () => ({ mutate: vi.fn(), isPending: false }),
useMarkConversationRead: () => ({ mutate: markReadMock, isPending: false }),
}));
vi.mock("@/hooks/use-websocket", () => ({
useA2ALiveStream: () => ({ lastMessage: null, isConnected: true }),
}));
vi.mock("@/hooks/use-tasks", () => ({
useTasks: () => ({ data: [] }),
}));
vi.mock("@/components/agents/agent-selector", () => ({
AgentSelector: () => <div data-testid="agent-selector" />,
}));
vi.mock("@/components/a2a/a2a-new-dm-dialog", () => ({
EXCLUDE_NON_DM_ROLES: [],
}));
function renderTab() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={client}>
<TgChatTab />
</QueryClientProvider>,
);
}
const mineRow = (over: Record<string, unknown> = {}) => ({
id: "c1",
other_agent: "main-pm",
topic: null,
task_id: null,
status: "active",
message_count: 2,
unread_count: 3,
last_message_at: new Date().toISOString(),
last_message_preview:
"**Wave 2** shipped for 33333333-3333-4333-8333-333333333333",
...over,
});
const fleetRow = (over: Record<string, unknown> = {}) => ({
id: "f1",
agent_a: "be-dev-1",
agent_b: "be-qa",
topic: "QA handoff",
task_id: "t-1",
status: "active",
message_count: 5,
last_message_at: new Date().toISOString(),
last_message_preview: "Suite is green.",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...over,
});
const msg = (over: Record<string, unknown> = {}) => ({
id: `m-${Math.random()}`,
conversation_id: "c1",
from_agent: "main-pm",
content: "Hello **there**",
message_kind: "text",
response_to_id: null,
requires_response: false,
read_at: null,
created_at: new Date().toISOString(),
edited_at: null,
...over,
});
beforeEach(() => {
mineItems.current = [];
fleetItems.current = [];
messages.current = [];
sendMock.mockReset();
replyMock.mockReset();
markReadMock.mockReset();
});
describe("TgChatTab — list", () => {
it("shows the CEO's own threads with unread badge and a groomed preview", () => {
mineItems.current = [mineRow()];
renderTab();
expect(screen.getByText("Main PM")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
// Markdown stripped, UUID shortened — never 36 raw chars.
const preview = screen.getByText(/Wave 2 shipped for #33333333/);
expect(preview.textContent).not.toContain("**");
expect(preview.textContent).not.toContain("-3333-");
});
it("Fleet scope lists agent↔agent threads and hides CEO pairs", async () => {
fleetItems.current = [
fleetRow(),
fleetRow({ id: "f2", agent_a: "ceo", agent_b: "main-pm" }),
];
renderTab();
await userEvent.click(screen.getByRole("button", { name: "Fleet" }));
expect(screen.getByText(/QA handoff/)).toBeInTheDocument();
// The CEO pair is Mine-only — never duplicated into Fleet.
expect(screen.queryByText(/Main PM/)).not.toBeInTheDocument();
});
});
describe("TgChatTab — threads", () => {
it("opens a Mine thread, renders agent markdown, clears unread", async () => {
mineItems.current = [mineRow()];
messages.current = [msg(), msg({ from_agent: "ceo", content: "Thanks" })];
renderTab();
await userEvent.click(screen.getByText("Main PM"));
expect(markReadMock).toHaveBeenCalledWith("c1");
// Agent message renders markdown (bold survives as <strong>).
expect(screen.getByText("there").tagName).toBe("STRONG");
// CEO bubble is plain text.
expect(screen.getByText("Thanks")).toBeInTheDocument();
});
it("sends into a Mine thread via the plain CEO send", async () => {
mineItems.current = [mineRow()];
renderTab();
await userEvent.click(screen.getByText("Main PM"));
await userEvent.type(screen.getByPlaceholderText("Message…"), "On it");
await userEvent.click(screen.getByRole("button", { name: "Send" }));
expect(sendMock).toHaveBeenCalledWith(
expect.objectContaining({ conversationId: "c1", content: "On it" }),
expect.anything(),
);
});
it("task-linked Fleet thread interjects via replyAsCeo with a recipient", async () => {
fleetItems.current = [fleetRow()];
messages.current = [msg({ conversation_id: "f1", from_agent: "be-dev-1" })];
renderTab();
await userEvent.click(screen.getByRole("button", { name: "Fleet" }));
await userEvent.click(screen.getByText(/QA handoff/));
// Default recipient = last non-CEO sender.
const chip = screen.getByRole("button", { name: /tap to switch/i });
expect(chip.textContent).toContain("Backend Dev 1");
await userEvent.type(screen.getByPlaceholderText("Message…"), "Status?");
await userEvent.click(screen.getByRole("button", { name: "Send" }));
expect(replyMock).toHaveBeenCalledWith(
expect.objectContaining({
conversationId: "f1",
to_agent: "be-dev-1",
content: "Status?",
}),
expect.anything(),
);
});
it("Fleet thread without a task link is watch-only", async () => {
fleetItems.current = [fleetRow({ task_id: null })];
renderTab();
await userEvent.click(screen.getByRole("button", { name: "Fleet" }));
await userEvent.click(screen.getByText(/QA handoff/));
expect(screen.getByText(/Watch-only/)).toBeInTheDocument();
expect(screen.queryByPlaceholderText("Message…")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,87 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TgInboxTab } from "../tg-inbox-tab";
import { NotificationPriority, NotificationType } from "@/types";
const { ackMock, items } = vi.hoisted(() => ({
ackMock: vi.fn(),
items: { current: [] as Array<Record<string, unknown>> },
}));
// No task in the shared index — every UUID falls back to the #id8 handle.
vi.mock("@/hooks/use-tasks", () => ({
useTasks: () => ({ data: [] }),
}));
vi.mock("@/hooks/use-notifications", () => ({
notificationKeys: { all: ["notifications"] },
useNotifications: () => ({
data: { items: items.current },
isLoading: false,
}),
useAcknowledgeNotification: () => ({ mutate: ackMock, isPending: false }),
}));
function notification(overrides: Record<string, unknown>) {
return {
id: "n1",
type: NotificationType.BROADCAST,
priority: NotificationPriority.NORMAL,
from_agent: "main-pm",
to_agents: ["ceo"],
subject: "A notification",
body: "body",
requires_ack: false,
is_acknowledged: false,
is_fully_acknowledged: false,
is_read: false,
related_task_id: null,
related_message_ids: [],
timestamp: new Date().toISOString(),
expires_at: null,
acked_by: [],
acked_at: {},
...overrides,
};
}
function renderTab() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={client}>
<TgInboxTab />
</QueryClientProvider>,
);
}
describe("TgInboxTab", () => {
it("humanizes an unresolved uuid subject to a short id handle", () => {
items.current = [
notification({
id: "n1",
subject: "Task 123e4567-e89b-12d3-a456-426614174000 needs review",
}),
];
renderTab();
expect(screen.getByText(/#123e4567/)).toBeInTheDocument();
});
it("splits a bracketed prefix into its own chip", () => {
items.current = [
notification({
id: "n1",
subject: "[strategy engine] weekly digest ready",
}),
];
renderTab();
expect(screen.getByText("Strategy engine")).toBeInTheDocument();
expect(screen.getByText("Weekly digest ready")).toBeInTheDocument();
});
it("shows inbox zero when there is nothing", () => {
items.current = [];
renderTab();
expect(screen.getByText(/inbox zero/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,91 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TgMetricsTab } from "../tg-metrics-tab";
vi.mock("@/lib/telegram/demo", () => ({ isTgDemoMode: () => true }));
// Scorecard resolution needs the roster only outside demo mode (the demo
// scorecard fixture returns unconditionally) — an empty roster keeps this
// hook off the network without affecting anything the tests assert on.
vi.mock("@/hooks/use-agents", () => ({ useAgents: () => ({ data: [] }) }));
function renderTab() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={client}>
<TgMetricsTab />
</QueryClientProvider>,
);
}
describe("TgMetricsTab", () => {
it("renders every hub section from the demo fixtures", async () => {
renderTab();
// Hero total — the demo agent/team/model/series slices all sum to $66.54.
expect(await screen.findByText("$66.54")).toBeInTheDocument();
expect(screen.getByText("By agent")).toBeInTheDocument();
expect(screen.getByText("By team")).toBeInTheDocument();
expect(screen.getByText("By model")).toBeInTheDocument();
expect(screen.getByText("Delivery")).toBeInTheDocument();
expect(screen.getByText("Efficiency")).toBeInTheDocument();
// Top agent by cost (be-dev-1, $18.42) renders first with its real name.
expect(await screen.findByText("Backend Dev 1")).toBeInTheDocument();
// A team row's exact label (distinct from "Backend Dev 1" above).
expect(await screen.findByText("Backend")).toBeInTheDocument();
});
it("switches the selected period on the segmented control", async () => {
renderTab();
await screen.findByText("$66.54");
const oneWeek = screen.getByRole("button", { name: "1W" });
const oneMonth = screen.getByRole("button", { name: "1M" });
expect(oneWeek).toHaveAttribute("aria-pressed", "true");
expect(oneMonth).toHaveAttribute("aria-pressed", "false");
await userEvent.click(oneMonth);
expect(oneMonth).toHaveAttribute("aria-pressed", "true");
expect(oneWeek).toHaveAttribute("aria-pressed", "false");
});
it("pushes the agent drilldown when a by-agent row is tapped", async () => {
renderTab();
await screen.findByText("$66.54");
await userEvent.click(await screen.findByText("Backend Dev 1"));
expect(
await screen.findByRole("heading", { name: "Backend Dev 1" }),
).toBeInTheDocument();
expect(screen.getByText("be-dev-1")).toBeInTheDocument();
});
it("shows the drilled-in agent's scorecard from the demo fixture", async () => {
renderTab();
await screen.findByText("$66.54");
await userEvent.click(await screen.findByText("Backend Dev 1"));
expect(await screen.findByText("Scorecard")).toBeInTheDocument();
expect(screen.getByText("14")).toBeInTheDocument(); // tasks_completed
});
it("returns to the hub from the drilldown's back button", async () => {
renderTab();
await screen.findByText("$66.54");
await userEvent.click(await screen.findByText("Backend Dev 1"));
await screen.findByRole("heading", { name: "Backend Dev 1" });
await userEvent.click(screen.getByRole("button", { name: "Back" }));
expect(await screen.findByText("By agent")).toBeInTheDocument();
expect(
screen.queryByRole("heading", { name: "Backend Dev 1" }),
).not.toBeInTheDocument();
});
});
@@ -3,20 +3,30 @@ import { render, screen, fireEvent } from "@testing-library/react";
import { TgTabBar } from "../tg-tab-bar";
describe("TgTabBar", () => {
it("renders all 4 tabs and marks the active one with aria-current", () => {
render(<TgTabBar active="inbox" onChange={vi.fn()} />);
it("renders all 5 tabs and marks the active one with aria-current", () => {
render(<TgTabBar active="metrics" onChange={vi.fn()} />);
expect(screen.getByRole("button", { name: /approvals/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /today/i })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /approvals/i }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: /board/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /chat/i })).toBeInTheDocument();
const inbox = screen.getByRole("button", { name: /inbox/i });
expect(inbox).toHaveAttribute("aria-current", "page");
const metrics = screen.getByRole("button", { name: /metrics/i });
expect(metrics).toHaveAttribute("aria-current", "page");
expect(
screen.getByRole("button", { name: /approvals/i }),
).not.toHaveAttribute("aria-current");
});
it("does not render Inbox as a tab (it lives behind the header bell)", () => {
render(<TgTabBar active="today" onChange={vi.fn()} />);
expect(
screen.queryByRole("button", { name: /inbox/i }),
).not.toBeInTheDocument();
});
it("calls onChange with the tapped tab's id", () => {
const onChange = vi.fn();
render(<TgTabBar active="approvals" onChange={onChange} />);
@@ -24,7 +34,7 @@ describe("TgTabBar", () => {
fireEvent.click(screen.getByRole("button", { name: /chat/i }));
expect(onChange).toHaveBeenCalledWith("chat");
fireEvent.click(screen.getByRole("button", { name: /board/i }));
expect(onChange).toHaveBeenCalledWith("board");
fireEvent.click(screen.getByRole("button", { name: /metrics/i }));
expect(onChange).toHaveBeenCalledWith("metrics");
});
});
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { render as rtlRender, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { TgTaskSheet } from "../tg-task-sheet";
import type { Task } from "@/types";
import type { TaskFindingsResponse } from "@/lib/api/tasks";
@@ -11,8 +12,19 @@ const { findings } = vi.hoisted(() => ({
}));
vi.mock("@/hooks/use-tasks", () => ({
useTaskFindings: findings,
taskKeys: { all: ["tasks"] },
}));
// The sheet's CEO action block mutates through react-query.
function render(ui: React.ReactElement) {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return rtlRender(
<QueryClientProvider client={client}>{ui}</QueryClientProvider>,
);
}
function task(overrides: Partial<Task> = {}): Task {
return {
id: "t1",
@@ -132,4 +144,32 @@ describe("TgTaskSheet", () => {
expect(screen.getByText("roboco/services/queue.py:42")).toBeInTheDocument();
expect(screen.queryByText(/dlq\.py/)).not.toBeInTheDocument();
});
it("offers Approve / Request changes on an awaiting-CEO task", () => {
render(<TgTaskSheet task={task()} onClose={vi.fn()} />);
expect(screen.getByRole("button", { name: "Approve" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Request changes" }),
).toBeInTheDocument();
});
it("offers Unblock on a blocked task and no CEO verbs elsewhere", () => {
render(
<TgTaskSheet
task={task({ status: "blocked" as Task["status"] })}
onClose={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: "Unblock" })).toBeInTheDocument();
render(
<TgTaskSheet
task={task({ id: "t2", status: "in_progress" as Task["status"] })}
onClose={vi.fn()}
/>,
);
expect(
screen.queryByRole("button", { name: "Approve" }),
).not.toBeInTheDocument();
});
});
@@ -142,7 +142,11 @@ describe("TgTodayTab", () => {
it("renders the operations ring and deep-links Ship into the release", async () => {
get.mockResolvedValue({
data: brief({
ship: { version: "0.25.0", open_release_proposal: true, ci_fix_tasks: 0 },
ship: {
version: "0.25.0",
open_release_proposal: true,
ci_fix_tasks: 0,
},
}),
});
const onNavigate = vi.fn();