Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/shared/src/inbox-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type SignalRecordKind =
| "ticket"
| "scanner_finding"
| "feedback"
| "review";
| "review"
| "search_opportunity";

/**
* A warehouse data source the Self-driving inbox can watch. This is the single source of
Expand Down Expand Up @@ -48,6 +49,7 @@ const ERROR = "Surface new and reopened errors";
const FINDING = "Surface new security and code-quality findings";
const FEEDBACK = "Turn product feedback and feature requests into inputs";
const REVIEW = "Monitor new app and product reviews";
const SEARCH = "Fix pages that rank in Google but lose clicks";

/** Registry of warehouse-backed inbox sources, alphabetical within each category. */
export const EXTERNAL_INBOX_SOURCES = [
Expand Down Expand Up @@ -392,6 +394,16 @@ export const EXTERNAL_INBOX_SOURCES = [
recordKind: "review",
setup: "dynamic",
},
// Search analytics
{
product: "google_search_console",
label: "Google Search Console",
description: SEARCH,
dwSourceType: "GoogleSearchConsole",
requiredTables: ["search_analytics_by_query_page"],
recordKind: "search_opportunity",
setup: "dynamic",
},
] as const satisfies readonly ExternalInboxSource[];

/** Warehouse-backed source products, derived from the registry above. */
Expand Down Expand Up @@ -434,9 +446,13 @@ export type SourceType =
| "session_analysis_cluster"
| SignalRecordKind;

/** Issue-like records mutate (status/votes change), so their table needs full-refresh sync. */
/**
* Issue-like records mutate (status/votes change), so their table needs full-refresh sync.
* Tickets and search-analytics rows are append-only — existing rows never change once written —
* so they sync incrementally.
*/
export function sourceNeedsFullRefresh(recordKind: SignalRecordKind): boolean {
return recordKind !== "ticket";
return recordKind !== "ticket" && recordKind !== "search_opportunity";
}

export const EXTERNAL_INBOX_SOURCE_BY_PRODUCT: Partial<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
KanbanIcon,
LifebuoyIcon,
LightbulbIcon,
MagnifyingGlassIcon,
MegaphoneIcon,
ShieldIcon,
StarIcon,
Expand Down Expand Up @@ -178,4 +179,9 @@ export const SOURCE_PRODUCT_META: Partial<
},
intercom: { Icon: ChatsIcon, color: "var(--blue-9)", label: "Intercom" },
hubspot: { Icon: LifebuoyIcon, color: "var(--orange-9)", label: "HubSpot" },
google_search_console: {
Icon: MagnifyingGlassIcon,
color: "var(--sky-9)",
label: "Google Search Console",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Task } from "@posthog/shared/domain-types";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import type { PropsWithChildren } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useRefreshedTask } from "./useRefreshedTask";

const mocks = vi.hoisted(() => ({ getTask: vi.fn() }));

vi.mock("@posthog/ui/features/auth/authClientImperative", () => ({
getAuthenticatedClient: vi.fn(async () => ({ getTask: mocks.getTask })),
}));

function task(runId: string, status: "failed" | "in_progress"): Task {
return {
id: "task-123",
title: "Cloud task",
description: "Keep working",
repository: null,
latest_run: {
id: runId,
task: "task-123",
environment: "cloud",
status,
state: {},
},
} as Task;
}

describe("useRefreshedTask", () => {
beforeEach(() => {
mocks.getTask.mockReset();
});

it("replaces a cached failed run with the authoritative resumed run", async () => {
const failedParent = task("run-parent", "failed");
const resumedChild = task("run-child", "in_progress");
mocks.getTask.mockResolvedValue(resumedChild);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

const { result } = renderHook(
() => useRefreshedTask("task-123", failedParent),
{ wrapper },
);

expect(result.current.latest_run?.id).toBe("run-parent");
await waitFor(() => {
expect(result.current.latest_run?.id).toBe("run-child");
});
expect(mocks.getTask).toHaveBeenCalledWith("task-123");
});
});
13 changes: 13 additions & 0 deletions packages/ui/src/features/task-detail/hooks/useRefreshedTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Task } from "@posthog/shared/domain-types";
import { useQuery } from "@tanstack/react-query";
import { taskDetailQuery } from "../../tasks/queries";

export function useRefreshedTask(taskId: string, initialTask: Task): Task {
const { data } = useQuery({
...taskDetailQuery(taskId),
initialData: initialTask,
refetchOnMount: "always",
});

return data;
}
10 changes: 2 additions & 8 deletions packages/ui/src/features/task-detail/hooks/useTaskData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { getTaskRepository } from "@posthog/shared";
import type { Task } from "@posthog/shared/domain-types";
import { useWorkspaceTRPC } from "@posthog/workspace-client/trpc";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { cloneStore } from "../../clone/cloneStore";
import { useTasks } from "../../tasks/useTasks";
import { useWorkspace } from "../../workspace/useWorkspace";
import { useRefreshedTask } from "./useRefreshedTask";

interface UseTaskDataParams {
taskId: string;
Expand All @@ -19,12 +18,7 @@ interface UseTaskDataParams {

export function useTaskData({ taskId, initialTask }: UseTaskDataParams) {
const trpcReact = useWorkspaceTRPC();
const { data: tasks = [] } = useTasks();

const task = useMemo(
() => tasks.find((t) => t.id === taskId) || initialTask,
[tasks, taskId, initialTask],
);
const task = useRefreshedTask(taskId, initialTask);

const workspace = useWorkspace(taskId);
const repoPath = workspace?.folderPath ?? null;
Expand Down
23 changes: 23 additions & 0 deletions packages/workspace-server/src/services/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,29 @@ describe("AgentService", () => {
expect(prompt).toContain("If the user names a folder or path");
});
});

describe("system prompt questions", () => {
it("requires blocking questions to use a structured user-input tool", () => {
const prompt = (
service as unknown as {
buildSystemPrompt: (
credentials: { apiHost: string; projectId: number },
taskId: string,
) => { append: string };
}
).buildSystemPrompt(
{ apiHost: "https://app.posthog.com", projectId: 1 },
"task-1",
).append;

expect(prompt).toContain(
"use the structured user-input tool available in your current mode",
);
expect(prompt).toContain(
"plain-text questions mark the task as finished",
);
});
});
});

describe("buildAutoApproveOutcome", () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/workspace-server/src/services/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,9 @@ When creating pull requests, add the following footer at the end of the PR descr

When you mention a pull request in any reply or summary, always hyperlink it to its full URL (e.g. a Markdown link like [#123](https://github.com/org/repo/pull/123)) rather than plain text, so readers can open it directly.

## Questions
When you need an answer from the user before you can continue, use the structured user-input tool available in your current mode. Never end a turn with a blocking question in a normal assistant message because plain-text questions mark the task as finished instead of waiting for the user's response.

## Shell efficiency
Optimize for the fewest shell round trips.
- Batch related commands into one Bash invocation using \`&&\` (e.g. \`npm run typecheck && npm run lint && npm test\`).
Expand Down
Loading