diff --git a/packages/ui/src/features/canvas/channelTaskSuggestions.ts b/packages/ui/src/features/canvas/channelTaskSuggestions.ts new file mode 100644 index 0000000000..5529b1447e --- /dev/null +++ b/packages/ui/src/features/canvas/channelTaskSuggestions.ts @@ -0,0 +1,93 @@ +import { + Bug, + ChartBar, + ChartLine, + ChatCircleText, + Cube, + CurrencyDollar, + Flask, + Wrench, +} from "@phosphor-icons/react"; +import type { SuggestedPrompt } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard"; + +// Starter prompts shown as cards on the channels (project-bluebird) new-task +// screen. Clicking a card drops its `prompt` into the composer, ready to +// edit/send. Each prompt ends with a "User input:" block of fill-in lines the +// user completes before sending. Channels-only — the /code new-task screen +// keeps its discovery suggestions. Card styling mirrors SuggestedTaskCard +// (icon badge + title + description); the icon/color follow the same +// `var(---N)` token scheme. +export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ + { + label: "Debug a user issue", + description: "Trace a specific user's events, replays, and errors", + icon: Bug, + color: "red", + mode: "auto", + prompt: + "Help me debug an issue a specific user is hitting. Pull their recent events, session replays, and errors, then figure out what went wrong.\n\n\nUser input:\n- Describe the user issue:\n- User identifier (distinct ID, email address, etc):", + }, + { + label: "Run a feature analysis", + description: "Adoption, engagement, and retention of a feature", + icon: ChartLine, + color: "blue", + mode: "auto", + prompt: + "Analyze how a feature is performing — adoption, engagement, and retention of users who use it vs. those who don't.\n\n\nUser input:\n- Feature to analyze:\n- Time period (optional):", + }, + { + label: "Understand revenue patterns", + description: "Trends over time, by plan, and by cohort", + icon: CurrencyDollar, + color: "green", + mode: "auto", + prompt: + "Analyze our revenue trends — break it down over time, by plan, and by cohort, and call out notable changes and likely drivers.\n\n\nUser input:\n- What revenue question are you trying to answer:\n- Time period (optional):", + }, + { + label: "Summarize product usage", + description: "Top events, active users, and key funnels", + icon: ChartBar, + color: "violet", + mode: "auto", + prompt: + "Summarize how our product is being used — top events, active users, key funnels, and notable trends.\n\n\nUser input:\n- Product area or feature to focus on (optional):\n- Time period (optional):", + }, + { + label: "Summarize user & agent feedback", + description: "Common themes across recent feedback", + icon: ChatCircleText, + color: "amber", + mode: "auto", + prompt: + "Summarize recent user and support/agent feedback — surface the common themes, complaints, and requests.\n\n\nUser input:\n- Feedback source or topic to focus on:\n- Time period (optional):", + }, + { + label: "Interpret experiment results", + description: "Significance and what to do next", + icon: Flask, + color: "purple", + mode: "auto", + prompt: + "Interpret the results of an experiment — explain what the metrics show, whether it's significant, and what to do next.\n\n\nUser input:\n- Experiment name or key:\n- What decision are you trying to make (optional):", + }, + { + label: "Fix a bug", + description: "Track down and fix a problem in the code", + icon: Wrench, + color: "orange", + mode: "plan", + prompt: + "Help me fix a bug — track down the root cause in the code and implement a fix. Open a PR if appropriate.\n\n\nUser input:\n- Describe the bug / what's going wrong:\n- Steps to reproduce (optional):\n- Where it happens (file, page, area — optional):", + }, + { + label: "Build a new feature", + description: "Design and implement something new", + icon: Cube, + color: "teal", + mode: "plan", + prompt: + "Help me build a new feature — propose an approach, then implement it. Open a PR if appropriate.\n\n\nUser input:\n- Describe the feature you want:\n- Any constraints or requirements (optional):", + }, +]; diff --git a/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx b/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx index 7b83b47324..c55fb03d42 100644 --- a/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx @@ -1,4 +1,5 @@ import type { Task } from "@posthog/shared/domain-types"; +import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions"; import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions"; @@ -47,6 +48,7 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) { channelContext={instructions?.content} channelName={channelName} allowNoRepo + suggestions={CHANNEL_TASK_SUGGESTIONS} /> ); } diff --git a/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx b/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx new file mode 100644 index 0000000000..b1b36e9ab2 --- /dev/null +++ b/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx @@ -0,0 +1,66 @@ +import type { Icon } from "@phosphor-icons/react"; +import type { ExecutionMode } from "@posthog/shared/domain-types"; +import { Flex, Text } from "@radix-ui/themes"; + +export interface SuggestedPrompt { + label: string; + description: string; + prompt: string; + icon: Icon; + color: string; + /** Task mode to apply when this suggestion is selected. */ + mode: ExecutionMode; +} + +export interface SuggestedPromptCardProps { + suggestion: SuggestedPrompt; + onSelect: () => void; +} + +// A starter-prompt card for the channels new-task screen. Mirrors the look of +// SuggestedTaskCard (icon badge + title + description), but clicking it fills +// the composer instead of opening a detail dialog. +export function SuggestedPromptCard({ + suggestion, + onSelect, +}: SuggestedPromptCardProps) { + const PromptIcon = suggestion.icon; + + return ( + + ); +} diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 5f997629c4..a508276afb 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -55,6 +55,10 @@ import { useInitialDirectoryFromFolderId } from "../hooks/useInitialDirectoryFro import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; import { CloudGithubMissingNotice } from "./CloudGithubMissingNotice"; +import { + type SuggestedPrompt, + SuggestedPromptCard, +} from "./SuggestedPromptCard"; import { SuggestedTasksPanel } from "./SuggestedTasksPanel"; import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect"; @@ -77,6 +81,12 @@ interface TaskInputProps { * needs a repo and attaches one lazily. */ allowNoRepo?: boolean; + /** + * Channels new-task starter prompts. When provided, a column of suggestion + * cards renders below the input while it's empty; clicking one fills the + * composer. Channels-only — omitted on the /code new-task screen. + */ + suggestions?: SuggestedPrompt[]; } export function TaskInput({ @@ -91,6 +101,7 @@ export function TaskInput({ channelContext, channelName, allowNoRepo, + suggestions, }: TaskInputProps = {}) { const cloudRegion = useAuthStateValue((s) => s.cloudRegion); const trpc = useHostTRPC(); @@ -532,6 +543,7 @@ export function TaskInput({ setAdditionalDirectories, } = useTaskCreation({ editorRef, + sessionId, selectedDirectory, selectedRepository: selectedCloudRepository, githubUserIntegrationId: selectedGithubUserIntegrationId, @@ -674,7 +686,14 @@ export function TaskInput({
0 && editorIsEmpty + ? "38%" + : "50%", transform: "translate(-50%, -50%)", }} className="absolute left-1/2 z-[1] flex w-[calc(100%-2rem)] max-w-[600px] flex-col gap-2" @@ -922,7 +941,47 @@ export function TaskInput({ )}
- + {suggestions ? ( + suggestions.length > 0 && + editorIsEmpty && ( +
+ + Suggestions + +
+ {suggestions.map((suggestion) => ( + { + // Use pending content (not setContent) so the + // multi-line template — intro + "User input:" fill-in + // lines — keeps its line breaks; focuses at the end. + useDraftStore + .getState() + .actions.setPendingContent(sessionId, { + segments: [ + { type: "text", text: suggestion.prompt }, + ], + }); + // Bug/feature suggestions start in plan mode; the + // analysis ones start in auto mode. + if (isValidConfigValue(modeOption, suggestion.mode)) { + setConfigOption(modeOption.id, suggestion.mode); + } + }} + /> + ))} +
+
+ ) + ) : ( + + )}
diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 8e5f1aa48c..c990e71361 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -35,6 +35,7 @@ import { type EditorContent, extractFilePaths, } from "../../message-editor/content"; +import { useDraftStore } from "../../message-editor/draftStore"; import { useTaskInputHistoryStore } from "../../message-editor/taskInputHistoryStore"; import type { EditorHandle } from "../../message-editor/types"; import { useSettingsStore } from "../../settings/settingsStore"; @@ -47,6 +48,8 @@ const log = logger.scope("task-creation"); interface UseTaskCreationOptions { editorRef: React.RefObject; + /** Draft-store session id for the editor; cleared on successful creation. */ + sessionId: string; selectedDirectory: string; selectedRepository?: string | null; githubIntegrationId?: number; @@ -132,6 +135,7 @@ async function trackTaskCreated( export function useTaskCreation({ editorRef, + sessionId, selectedDirectory, selectedRepository, githubIntegrationId, @@ -290,15 +294,19 @@ export function useTaskCreation({ if (pendingTaskKey) { pendingTaskPromptStoreApi.move(pendingTaskKey, output.task.id); } + // Clear the draft BEFORE navigating away. When onTaskCreated + // navigates (e.g. channels), it can synchronously unmount/destroy + // the editor; clearing afterwards would throw in clearContent() + // before the persisted draft is wiped, leaving stale text behind. + if (!pendingTaskKey && !contentOverride) { + editor.clear(); + } if (onTaskCreated) { onTaskCreated(output.task); } else { void openTask(output.task); } useTourStore.getState().completeTour(createFirstTaskTour.id); - if (!pendingTaskKey && !contentOverride) { - editor.clear(); - } // Pre-flight already ran above for cloud; skip the service's duplicate check. }, { skipCloudUsagePreflight: true }, @@ -306,6 +314,14 @@ export function useTaskCreation({ if (result.success) { setAdditionalDirectoriesOverride(null); + // Guarantee the editor draft is wiped on success. editor.clear() + // above only runs inside the onTaskReady callback (and after it + // navigates the editor may be torn down); clearing the persisted + // draft directly here always runs and survives the unmount, so a + // submitted prompt never reappears on the next new task. + if (!contentOverride) { + useDraftStore.getState().actions.setDraft(sessionId, null); + } void trackTaskCreated(input, selectedDirectory, hostClient); // Repo-less channel tasks create no workspace row (the agent runs in // a scratch dir surfaced as a synthetic workspace), so the normal @@ -355,6 +371,7 @@ export function useTaskCreation({ canSubmit, canSubmitBase, editorRef, + sessionId, selectedDirectory, selectedRepository, githubIntegrationId, diff --git a/packages/ui/src/router/routes/website/new.tsx b/packages/ui/src/router/routes/website/new.tsx index 1211854afe..0138523a67 100644 --- a/packages/ui/src/router/routes/website/new.tsx +++ b/packages/ui/src/router/routes/website/new.tsx @@ -1,3 +1,4 @@ +import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions"; import { TaskInput } from "@posthog/ui/features/task-detail/components/TaskInput"; import { useAppView } from "@posthog/ui/router/useAppView"; import { createFileRoute } from "@tanstack/react-router"; @@ -21,6 +22,7 @@ function WebsiteNewTaskRoute() { initialModel={view.initialModel} initialMode={view.initialMode} reportAssociation={view.reportAssociation} + suggestions={CHANNEL_TASK_SUGGESTIONS} /> ); }