diff --git a/components/Chat/ChatGreeting.tsx b/components/Chat/ChatGreeting.tsx
index dbe43911d..e95c002f4 100644
--- a/components/Chat/ChatGreeting.tsx
+++ b/components/Chat/ChatGreeting.tsx
@@ -2,13 +2,17 @@ import { useArtistProvider } from "@/providers/ArtistProvider";
import ImageWithFallback from "@/components/ImageWithFallback";
import ValuationHero from "@/components/Home/ValuationHero";
import useHomeValuation from "@/hooks/useHomeValuation";
+import TasksModule from "@/components/Home/TasksModule";
import { VALUATION_URL } from "@/lib/consts";
/**
- * The empty chat's artist header (recoupable/chat#1850): the artist-scoped
- * catalog valuation hero when the account has measured data for the current
- * scope, otherwise the "Ask me about" greeting plus a CTA into the valuation
- * funnel. All context arrives via provider-backed hooks — no props beyond
+ * The empty chat's home header (recoupable/chat#1850), valuation first —
+ * it's the headline number customers arrive with from the marketing
+ * funnel: the artist-scoped catalog valuation hero when the account has
+ * measured data for the current scope, otherwise the "Ask me about"
+ * greeting plus a CTA into the valuation funnel; the "label at work"
+ * tasks module renders beneath it (self-hiding when it has nothing to
+ * show). All context arrives via provider-backed hooks — no props beyond
* the fade flag the chat empty state already owned, so the chat component
* needs no knowledge of what this header shows.
*/
@@ -24,13 +28,14 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
if (valuation.show) {
return (
-
+
+
);
}
@@ -60,7 +65,7 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
)}
{isArtistSelected ? artistName : "your roster"}
-
);
}
diff --git a/components/Home/HomeRunRow.tsx b/components/Home/HomeRunRow.tsx
new file mode 100644
index 000000000..3ecf555ed
--- /dev/null
+++ b/components/Home/HomeRunRow.tsx
@@ -0,0 +1,39 @@
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import type { TaskRunItem } from "@/lib/tasks/getTaskRuns";
+import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName";
+import { getStatusColor } from "@/lib/tasks/getStatusColor";
+import { getStatusLabel } from "@/lib/tasks/getStatusLabel";
+import { formatTimestamp } from "@/lib/tasks/formatTimestamp";
+
+/**
+ * Compact task-run row for the homepage tasks module. Links to the
+ * existing run detail page. Per-run sent-email subjects are not exposed
+ * by `GET /api/tasks/runs` yet, so this shows what is available: run
+ * name, status, and finished-at (recoupable/chat#1850).
+ */
+const HomeRunRow = ({ run }: { run: TaskRunItem }) => (
+
+
+
+ {getTaskDisplayName(run.taskIdentifier)}
+
+
+ {formatTimestamp(run.finishedAt ?? run.createdAt)}
+
+
+
+ {getStatusLabel(run.status)}
+
+
+);
+
+export default HomeRunRow;
diff --git a/components/Home/StarterTaskCard.tsx b/components/Home/StarterTaskCard.tsx
new file mode 100644
index 000000000..5a6cfd5bc
--- /dev/null
+++ b/components/Home/StarterTaskCard.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useArtistProvider } from "@/providers/ArtistProvider";
+import { useCreateStarterTask } from "@/hooks/useCreateStarterTask";
+
+/**
+ * Empty-state suggestion for the homepage tasks module: one click
+ * schedules an existing /agents Report template for the selected artist,
+ * Mondays at 9am, via the existing task-creation path
+ * (recoupable/chat#1850). Hidden when no Report template exists.
+ */
+const StarterTaskCard = () => {
+ const { selectedArtist } = useArtistProvider();
+ const { handleCreateStarterTask, isCreating, isScheduled, starterTemplate } =
+ useCreateStarterTask();
+ const artistName = selectedArtist?.name || "your artist";
+
+ if (!starterTemplate) return null;
+
+ if (isScheduled) {
+ return (
+
+ Scheduled: {starterTemplate.title} for {artistName}, Mondays at 9am.
+
+ );
+ }
+
+ return (
+
+
+
+ {starterTemplate.title} for {artistName}, Mondays
+
+
+ {starterTemplate.description}
+
+
+
handleCreateStarterTask()}
+ disabled={isCreating}
+ className="shrink-0 rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
+ >
+ {isCreating ? "Scheduling..." : "Schedule it"}
+
+
+ );
+};
+
+export default StarterTaskCard;
diff --git a/components/Home/TasksModule.tsx b/components/Home/TasksModule.tsx
new file mode 100644
index 000000000..f06a7eeec
--- /dev/null
+++ b/components/Home/TasksModule.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import Link from "next/link";
+import { useArtistProvider } from "@/providers/ArtistProvider";
+import { useTaskRuns } from "@/hooks/useTaskRuns";
+import { getHomeTasksModuleState } from "@/lib/home/getHomeTasksModuleState";
+import HomeRunRow from "./HomeRunRow";
+import StarterTaskCard from "./StarterTaskCard";
+
+/**
+ * "Your label at work" homepage module: recent task runs (existing
+ * GET /api/tasks/runs) or the one-click starter suggestion for fresh
+ * accounts (recoupable/chat#1850). Renders nothing while loading or on
+ * failure so the homepage never blocks on it.
+ */
+const TasksModule = () => {
+ const { data: runs, isLoading, isError } = useTaskRuns();
+ const { selectedArtist } = useArtistProvider();
+
+ const state = getHomeTasksModuleState({
+ runs,
+ runsFailed: isError,
+ isLoading,
+ hasArtist: !!selectedArtist?.account_id,
+ });
+
+ if (state.view === "hidden") return null;
+
+ return (
+
+
+
+ Your label at work
+
+
+ View all
+
+
+ {state.view === "runs" ? (
+
+ {state.runs.map((run) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default TasksModule;
diff --git a/hooks/useCreateStarterTask.ts b/hooks/useCreateStarterTask.ts
new file mode 100644
index 000000000..43fb29993
--- /dev/null
+++ b/hooks/useCreateStarterTask.ts
@@ -0,0 +1,79 @@
+"use client";
+
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { usePrivy } from "@privy-io/react-auth";
+import { toast } from "sonner";
+import { createStarterTask } from "@/lib/home/createStarterTask";
+import { findStarterTemplate } from "@/lib/home/findStarterTemplate";
+import fetchAgentTemplates from "@/lib/agent-templates/fetchAgentTemplates";
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+import { useArtistProvider } from "@/providers/ArtistProvider";
+import { useUserProvider } from "@/providers/UserProvder";
+
+/**
+ * One-click creation of the homepage starter task. The task is sourced
+ * from an existing /agents template (DRY — findStarterTemplate picks it
+ * from the shared agent-templates query); auth + artist come from
+ * providers and the mutation goes through the existing `POST /api/tasks`
+ * path which also mints the schedule (recoupable/chat#1850).
+ */
+export function useCreateStarterTask() {
+ const { getAccessToken } = usePrivy();
+ const { selectedArtist } = useArtistProvider();
+ const { userData } = useUserProvider();
+ const queryClient = useQueryClient();
+
+ const { data: templates } = useQuery
({
+ queryKey: ["agent-templates"],
+ queryFn: () => fetchAgentTemplates(userData!),
+ retry: 1,
+ enabled: !!userData?.id,
+ });
+ const starterTemplate = findStarterTemplate(templates);
+
+ const {
+ mutate: handleCreateStarterTask,
+ isPending: isCreating,
+ isSuccess: isScheduled,
+ } = useMutation({
+ mutationFn: async () => {
+ const artistAccountId = selectedArtist?.account_id;
+ const artistName = selectedArtist?.name;
+ if (!artistAccountId || !artistName) {
+ throw new Error("ARTIST_REQUIRED");
+ }
+ if (!starterTemplate) {
+ throw new Error("TEMPLATE_REQUIRED");
+ }
+ const accessToken = await getAccessToken();
+ if (!accessToken) {
+ throw new Error("AUTH_REQUIRED");
+ }
+ return createStarterTask(accessToken, {
+ template: starterTemplate,
+ artistName,
+ artistAccountId,
+ });
+ },
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: ["scheduled-actions"],
+ exact: false,
+ });
+ toast.success("Task scheduled for Mondays at 9am");
+ },
+ onError: (error) => {
+ if (error instanceof Error && error.message === "ARTIST_REQUIRED") {
+ toast.error("Please select an artist first.");
+ return;
+ }
+ if (error instanceof Error && error.message === "AUTH_REQUIRED") {
+ toast.error("Please sign in to create a task.");
+ return;
+ }
+ toast.error("Failed to create the task. Please try again.");
+ },
+ });
+
+ return { handleCreateStarterTask, isCreating, isScheduled, starterTemplate };
+}
diff --git a/lib/home/__tests__/buildStarterTaskParams.test.ts b/lib/home/__tests__/buildStarterTaskParams.test.ts
new file mode 100644
index 000000000..1679a6ce0
--- /dev/null
+++ b/lib/home/__tests__/buildStarterTaskParams.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from "vitest";
+import { buildStarterTaskParams } from "@/lib/home/buildStarterTaskParams";
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+
+const template = {
+ id: "9046c7e9-fd5a-4f08-a472-381d51bd6c90",
+ title: "Weekly Performance Dashboard",
+ description: "Weekly stats email",
+ prompt:
+ "Set up a weekly performance dashboard that emails me every Monday with my stats from all platforms.",
+ tags: ["Report"],
+ creator: null,
+ is_private: false,
+ created_at: null,
+ favorites_count: null,
+ updated_at: null,
+} as AgentTemplateRow;
+
+describe("buildStarterTaskParams", () => {
+ const params = buildStarterTaskParams({
+ template,
+ artistName: "Del Water Gap",
+ artistAccountId: "artist-1",
+ });
+
+ it("titles the task after the template and the artist", () => {
+ expect(params.title).toBe("Weekly Performance Dashboard — Del Water Gap");
+ });
+
+ it("uses the agent template's prompt verbatim (DRY — no hand-written prompt)", () => {
+ expect(params.prompt).toBe(template.prompt);
+ });
+
+ it("schedules Mondays", () => {
+ expect(params.schedule).toBe("0 9 * * 1");
+ });
+
+ it("targets the artist account", () => {
+ expect(params.artist_account_id).toBe("artist-1");
+ });
+});
diff --git a/lib/home/__tests__/createStarterTask.test.ts b/lib/home/__tests__/createStarterTask.test.ts
new file mode 100644
index 000000000..98222be57
--- /dev/null
+++ b/lib/home/__tests__/createStarterTask.test.ts
@@ -0,0 +1,83 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createStarterTask } from "@/lib/home/createStarterTask";
+import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl";
+import { DEFAULT_MODEL } from "@/lib/consts";
+
+vi.mock("@/lib/api/getClientApiBaseUrl", () => ({
+ getClientApiBaseUrl: vi.fn(),
+}));
+
+const starterTemplate = {
+ id: "9046c7e9-fd5a-4f08-a472-381d51bd6c90",
+ title: "Weekly Performance Dashboard",
+ description: "Weekly stats email",
+ prompt:
+ "Set up a weekly performance dashboard that emails me every Monday with my stats from all platforms.",
+ tags: ["Report"],
+ creator: null,
+ is_private: false,
+ created_at: null,
+ favorites_count: null,
+ updated_at: null,
+} as import("@/types/AgentTemplates").AgentTemplateRow;
+
+describe("createStarterTask", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(getClientApiBaseUrl).mockReturnValue(
+ "https://api.recoupable.com",
+ );
+ });
+
+ it("POSTs the pre-wired weekly report task to /api/tasks", async () => {
+ const createdTask = { id: "task-1" };
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({
+ status: "success",
+ tasks: [createdTask],
+ }),
+ }) as unknown as typeof fetch;
+
+ const result = await createStarterTask("test-token", {
+ template: starterTemplate,
+ artistName: "Del Water Gap",
+ artistAccountId: "artist-1",
+ });
+
+ expect(fetch).toHaveBeenCalledWith(
+ "https://api.recoupable.com/api/tasks",
+ expect.objectContaining({
+ method: "POST",
+ headers: {
+ Authorization: "Bearer test-token",
+ "Content-Type": "application/json",
+ },
+ }),
+ );
+ const init = vi.mocked(fetch).mock.calls[0][1] as RequestInit;
+ const body = JSON.parse(String(init.body));
+ expect(body.title).toBe("Weekly Performance Dashboard — Del Water Gap");
+ expect(body.schedule).toBe("0 9 * * 1");
+ expect(body.artist_account_id).toBe("artist-1");
+ expect(body.model).toBe(DEFAULT_MODEL);
+ expect(body.prompt).toBe(starterTemplate.prompt);
+ expect(result).toEqual(createdTask);
+ });
+
+ it("propagates API errors", async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: false,
+ status: 500,
+ text: vi.fn().mockResolvedValue("boom"),
+ }) as unknown as typeof fetch;
+
+ await expect(
+ createStarterTask("test-token", {
+ template: starterTemplate,
+ artistName: "Del Water Gap",
+ artistAccountId: "artist-1",
+ }),
+ ).rejects.toThrow("HTTP 500");
+ });
+});
diff --git a/lib/home/__tests__/findStarterTemplate.test.ts b/lib/home/__tests__/findStarterTemplate.test.ts
new file mode 100644
index 000000000..36a2bc7bd
--- /dev/null
+++ b/lib/home/__tests__/findStarterTemplate.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from "vitest";
+import { findStarterTemplate } from "@/lib/home/findStarterTemplate";
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+
+const template = (over: Partial): AgentTemplateRow =>
+ ({
+ id: "t-1",
+ title: "Some Agent",
+ description: "",
+ prompt: "do things",
+ tags: null,
+ creator: null,
+ is_private: false,
+ created_at: null,
+ favorites_count: null,
+ updated_at: null,
+ ...over,
+ }) as AgentTemplateRow;
+
+describe("findStarterTemplate", () => {
+ it("prefers the Weekly Performance Dashboard template", () => {
+ const templates = [
+ template({ id: "a", title: "Run Performance Report", tags: ["Report"] }),
+ template({
+ id: "b",
+ title: "Weekly Performance Dashboard",
+ tags: ["Report"],
+ }),
+ ];
+ expect(findStarterTemplate(templates)?.id).toBe("b");
+ });
+
+ it("falls back to the first Report-tagged template", () => {
+ const templates = [
+ template({ id: "a", title: "5 Viral Content Ideas", tags: ["Create"] }),
+ template({ id: "b", title: "YouTube Revenue Report", tags: ["Report"] }),
+ ];
+ expect(findStarterTemplate(templates)?.id).toBe("b");
+ });
+
+ it("returns undefined when no Report template exists", () => {
+ expect(
+ findStarterTemplate([template({ tags: ["Create"] })]),
+ ).toBeUndefined();
+ expect(findStarterTemplate([])).toBeUndefined();
+ expect(findStarterTemplate(undefined)).toBeUndefined();
+ });
+});
diff --git a/lib/home/__tests__/getHomeTasksModuleState.test.ts b/lib/home/__tests__/getHomeTasksModuleState.test.ts
new file mode 100644
index 000000000..d53da4093
--- /dev/null
+++ b/lib/home/__tests__/getHomeTasksModuleState.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it } from "vitest";
+import { getHomeTasksModuleState } from "@/lib/home/getHomeTasksModuleState";
+import type { TaskRunItem } from "@/lib/tasks/getTaskRuns";
+
+const makeRun = (id: string): TaskRunItem => ({
+ id,
+ status: "COMPLETED",
+ taskIdentifier: "customer-prompt-task",
+ createdAt: "2026-07-06T09:00:00Z",
+ startedAt: "2026-07-06T09:00:01Z",
+ finishedAt: "2026-07-06T09:05:00Z",
+ durationMs: 299000,
+});
+
+describe("getHomeTasksModuleState", () => {
+ it("hides the module while runs are loading", () => {
+ expect(
+ getHomeTasksModuleState({
+ runs: undefined,
+ runsFailed: false,
+ isLoading: true,
+ hasArtist: true,
+ }),
+ ).toEqual({ view: "hidden" });
+ });
+
+ it("hides the module when the runs request failed", () => {
+ expect(
+ getHomeTasksModuleState({
+ runs: undefined,
+ runsFailed: true,
+ isLoading: false,
+ hasArtist: true,
+ }),
+ ).toEqual({ view: "hidden" });
+ });
+
+ it("shows recent runs when the account has task history", () => {
+ const runs = ["a", "b", "c"].map(makeRun);
+ expect(
+ getHomeTasksModuleState({
+ runs,
+ runsFailed: false,
+ isLoading: false,
+ hasArtist: true,
+ }),
+ ).toEqual({ view: "runs", runs });
+ });
+
+ it("caps the module at five runs", () => {
+ const runs = ["a", "b", "c", "d", "e", "f", "g"].map(makeRun);
+ const state = getHomeTasksModuleState({
+ runs,
+ runsFailed: false,
+ isLoading: false,
+ hasArtist: true,
+ });
+ expect(state).toEqual({ view: "runs", runs: runs.slice(0, 5) });
+ });
+
+ it("offers the starter task to a fresh account with an artist", () => {
+ expect(
+ getHomeTasksModuleState({
+ runs: [],
+ runsFailed: false,
+ isLoading: false,
+ hasArtist: true,
+ }),
+ ).toEqual({ view: "starter" });
+ });
+
+ it("hides the starter card when no artist is selected", () => {
+ expect(
+ getHomeTasksModuleState({
+ runs: [],
+ runsFailed: false,
+ isLoading: false,
+ hasArtist: false,
+ }),
+ ).toEqual({ view: "hidden" });
+ });
+});
diff --git a/lib/home/buildStarterTaskParams.ts b/lib/home/buildStarterTaskParams.ts
new file mode 100644
index 000000000..55258f748
--- /dev/null
+++ b/lib/home/buildStarterTaskParams.ts
@@ -0,0 +1,27 @@
+import type { CreateTaskParams } from "@/lib/tasks/createTask";
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+
+interface BuildStarterTaskParamsInput {
+ template: AgentTemplateRow;
+ artistName: string;
+ artistAccountId: string;
+}
+
+/**
+ * The pre-wired one-click suggestion shown in the homepage tasks module's
+ * empty state (recoupable/chat#1850). The prompt is the chosen /agents
+ * template's prompt verbatim (DRY — see findStarterTemplate); only the
+ * title is personalized so the task list reads per-artist.
+ */
+export function buildStarterTaskParams({
+ template,
+ artistName,
+ artistAccountId,
+}: BuildStarterTaskParamsInput): Omit {
+ return {
+ title: `${template.title} — ${artistName}`,
+ prompt: template.prompt,
+ schedule: "0 9 * * 1",
+ artist_account_id: artistAccountId,
+ };
+}
diff --git a/lib/home/createStarterTask.ts b/lib/home/createStarterTask.ts
new file mode 100644
index 000000000..e2384bd05
--- /dev/null
+++ b/lib/home/createStarterTask.ts
@@ -0,0 +1,26 @@
+import { createTask } from "@/lib/tasks/createTask";
+import type { Task } from "@/lib/tasks/getTasks";
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+import { buildStarterTaskParams } from "@/lib/home/buildStarterTaskParams";
+import { DEFAULT_MODEL } from "@/lib/consts";
+
+interface CreateStarterTaskInput {
+ template: AgentTemplateRow;
+ artistName: string;
+ artistAccountId: string;
+}
+
+/**
+ * Creates the homepage starter task through the existing task-creation
+ * path (`POST /api/tasks`, which also mints the schedule). The task is
+ * built from an existing /agents template (DRY).
+ */
+export async function createStarterTask(
+ accessToken: string,
+ input: CreateStarterTaskInput,
+): Promise {
+ return createTask(accessToken, {
+ ...buildStarterTaskParams(input),
+ model: DEFAULT_MODEL,
+ });
+}
diff --git a/lib/home/findStarterTemplate.ts b/lib/home/findStarterTemplate.ts
new file mode 100644
index 000000000..78c014f28
--- /dev/null
+++ b/lib/home/findStarterTemplate.ts
@@ -0,0 +1,20 @@
+import type { AgentTemplateRow } from "@/types/AgentTemplates";
+
+const PREFERRED_TITLE = "Weekly Performance Dashboard";
+
+/**
+ * Picks the agent template behind the homepage starter task (DRY — the
+ * task's prompt comes from the existing /agents templates, never a
+ * hand-written string; recoupable/chat#1850 review). Prefers the weekly
+ * dashboard template, falls back to any Report-tagged agent, and returns
+ * undefined (card hidden) when none exist.
+ */
+export function findStarterTemplate(
+ templates: AgentTemplateRow[] | undefined,
+): AgentTemplateRow | undefined {
+ if (!templates?.length) return undefined;
+ return (
+ templates.find((t) => t.title === PREFERRED_TITLE) ??
+ templates.find((t) => (t.tags ?? []).includes("Report"))
+ );
+}
diff --git a/lib/home/getHomeTasksModuleState.ts b/lib/home/getHomeTasksModuleState.ts
new file mode 100644
index 000000000..b33004b25
--- /dev/null
+++ b/lib/home/getHomeTasksModuleState.ts
@@ -0,0 +1,35 @@
+import type { TaskRunItem } from "@/lib/tasks/getTaskRuns";
+
+const MAX_HOME_RUNS = 5;
+
+interface GetHomeTasksModuleStateParams {
+ runs: TaskRunItem[] | undefined;
+ runsFailed: boolean;
+ isLoading: boolean;
+ hasArtist: boolean;
+}
+
+export type HomeTasksModuleState =
+ | { view: "hidden" }
+ | { view: "runs"; runs: TaskRunItem[] }
+ | { view: "starter" };
+
+/**
+ * Decides what the homepage tasks module renders: recent runs for accounts
+ * with task history, the one-click starter suggestion for fresh accounts
+ * with an artist, or nothing while loading / on failure so the homepage
+ * never blocks on this module (recoupable/chat#1850).
+ */
+export function getHomeTasksModuleState({
+ runs,
+ runsFailed,
+ isLoading,
+ hasArtist,
+}: GetHomeTasksModuleStateParams): HomeTasksModuleState {
+ if (isLoading || runsFailed || !runs) return { view: "hidden" };
+
+ if (runs.length > 0)
+ return { view: "runs", runs: runs.slice(0, MAX_HOME_RUNS) };
+
+ return hasArtist ? { view: "starter" } : { view: "hidden" };
+}