diff --git a/packages/shared/src/inbox-types.ts b/packages/shared/src/inbox-types.ts index f451fbe515..89501ced1c 100644 --- a/packages/shared/src/inbox-types.ts +++ b/packages/shared/src/inbox-types.ts @@ -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 @@ -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 = [ @@ -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. */ @@ -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< diff --git a/packages/ui/src/features/inbox/components/utils/source-product-icons.tsx b/packages/ui/src/features/inbox/components/utils/source-product-icons.tsx index a480c99c3c..a30a065c50 100644 --- a/packages/ui/src/features/inbox/components/utils/source-product-icons.tsx +++ b/packages/ui/src/features/inbox/components/utils/source-product-icons.tsx @@ -10,6 +10,7 @@ import { KanbanIcon, LifebuoyIcon, LightbulbIcon, + MagnifyingGlassIcon, MegaphoneIcon, ShieldIcon, StarIcon, @@ -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", + }, }; diff --git a/packages/ui/src/features/task-detail/hooks/useRefreshedTask.test.tsx b/packages/ui/src/features/task-detail/hooks/useRefreshedTask.test.tsx new file mode 100644 index 0000000000..9d11d1c35a --- /dev/null +++ b/packages/ui/src/features/task-detail/hooks/useRefreshedTask.test.tsx @@ -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) => ( + {children} + ); + + 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"); + }); +}); diff --git a/packages/ui/src/features/task-detail/hooks/useRefreshedTask.ts b/packages/ui/src/features/task-detail/hooks/useRefreshedTask.ts new file mode 100644 index 0000000000..e2f6be477d --- /dev/null +++ b/packages/ui/src/features/task-detail/hooks/useRefreshedTask.ts @@ -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; +} diff --git a/packages/ui/src/features/task-detail/hooks/useTaskData.ts b/packages/ui/src/features/task-detail/hooks/useTaskData.ts index ae278ba306..477dd1e704 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskData.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskData.ts @@ -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; @@ -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; diff --git a/packages/workspace-server/src/services/agent/agent.test.ts b/packages/workspace-server/src/services/agent/agent.test.ts index 293ac3854e..14204e4065 100644 --- a/packages/workspace-server/src/services/agent/agent.test.ts +++ b/packages/workspace-server/src/services/agent/agent.test.ts @@ -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", () => { diff --git a/packages/workspace-server/src/services/agent/agent.ts b/packages/workspace-server/src/services/agent/agent.ts index a352357e3b..9fa5c7db8a 100644 --- a/packages/workspace-server/src/services/agent/agent.ts +++ b/packages/workspace-server/src/services/agent/agent.ts @@ -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\`).