Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/shared/src/workspace-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export const workspaceSchema = z.object({
baseBranch: z.string().nullable(),
linkedBranch: z.string().nullable(),
createdAt: z.string(),
/**
* Synthetic workspace for a repo-less channel task: its folderPath is a
* scratch dir, not a registered folder. Marks it so callers (e.g. the
* navigation task binder) don't try to register it as a folder or git-init it.
*/
isScratch: z.boolean().optional(),
});

export type WorktreeInfo = z.infer<typeof worktreeInfoSchema>;
Expand Down
93 changes: 93 additions & 0 deletions packages/ui/src/features/canvas/channelTaskSuggestions.ts
Original file line number Diff line number Diff line change
@@ -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(--<color>-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):",
},
];
2 changes: 2 additions & 0 deletions packages/ui/src/features/canvas/components/WebsiteNewTask.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -47,6 +48,7 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) {
channelContext={instructions?.content}
channelName={channelName}
allowNoRepo
suggestions={CHANNEL_TASK_SUGGESTIONS}
/>
);
}
8 changes: 8 additions & 0 deletions packages/ui/src/features/navigation/taskBinderImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export const navigationTaskBinder: NavigationTaskBinder = {

const workspaces = await hostClient().workspace.getAll.query();
const existingWorkspace = workspaces?.[task.id] ?? null;

// Repo-less channel task: its workspace is a synthetic scratch dir, not a
// registered folder. Never register it (that would pop the "initialize git"
// dialog on the empty scratch dir) or write a workspace row for it.
if (existingWorkspace?.isScratch) {
return undefined;
}

if (existingWorkspace?.folderId) {
const folders = await hostClient().folders.getFolders.query();
const folder = folders.find((f) => f.id === existingWorkspace.folderId);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
onClick={onSelect}
className="flex w-full cursor-pointer items-start gap-2.5 rounded-xl border border-(--gray-a3) bg-(--color-panel-solid) px-2.5 py-2 text-left shadow-[0_1px_3px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.02)] transition-[border-color,box-shadow] hover:border-(--card-hover-border) hover:shadow-[0_2px_8px_rgba(0,0,0,0.06),0_1px_3px_rgba(0,0,0,0.04)]"
style={
{
"--card-hover-border": `var(--${suggestion.color}-6)`,
} as React.CSSProperties
}
>
<Flex
align="center"
justify="center"
className="h-6 w-6 shrink-0 rounded-md"
style={{ backgroundColor: `var(--${suggestion.color}-3)` }}
>
<PromptIcon
size={14}
weight="duotone"
color={`var(--${suggestion.color}-9)`}
/>
</Flex>
<Flex direction="column" gap="1" className="min-w-0 flex-1">
<Text
size="1"
weight="medium"
className="min-w-0 truncate text-(--gray-12)"
>
{suggestion.label}
</Text>
<Text size="1" className="line-clamp-1 text-(--gray-11) leading-normal">
{suggestion.description}
</Text>
</Flex>
</button>
);
}
63 changes: 61 additions & 2 deletions packages/ui/src/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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({
Expand All @@ -91,6 +101,7 @@ export function TaskInput({
channelContext,
channelName,
allowNoRepo,
suggestions,
}: TaskInputProps = {}) {
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const trpc = useHostTRPC();
Expand Down Expand Up @@ -532,6 +543,7 @@ export function TaskInput({
setAdditionalDirectories,
} = useTaskCreation({
editorRef,
sessionId,
selectedDirectory,
selectedRepository: selectedCloudRepository,
githubUserIntegrationId: selectedGithubUserIntegrationId,
Expand Down Expand Up @@ -674,7 +686,14 @@ export function TaskInput({
<DotPatternBackground className="h-[100.333%]" />
<div
style={{
top: "50%",
// Raise the input when the suggestion cards are shown so the longer
// list below it isn't squished against the bottom of the viewport.
// Tied to the same condition as the cards (which hide once the
// editor has content) so the input recenters as the user types.
top:
suggestions && suggestions.length > 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"
Expand Down Expand Up @@ -922,7 +941,47 @@ export function TaskInput({
)}
</Flex>
<div className="absolute top-full right-0 left-0 z-10">
<SuggestedTasksPanel />
{suggestions ? (
suggestions.length > 0 &&
editorIsEmpty && (
<div className="mt-6 flex flex-col gap-2">
<Text
size="1"
weight="medium"
className="px-2.5 text-(--gray-11)"
>
Suggestions
</Text>
<div className="grid grid-cols-2 gap-2">
{suggestions.map((suggestion) => (
<SuggestedPromptCard
key={suggestion.label}
suggestion={suggestion}
onSelect={() => {
// 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);
}
}}
/>
))}
</div>
</div>
)
) : (
<SuggestedTasksPanel />
)}
</div>
</div>
</Flex>
Expand Down
23 changes: 20 additions & 3 deletions packages/ui/src/features/task-detail/hooks/useTaskCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -47,6 +48,8 @@ const log = logger.scope("task-creation");

interface UseTaskCreationOptions {
editorRef: React.RefObject<EditorHandle | null>;
/** Draft-store session id for the editor; cleared on successful creation. */
sessionId: string;
selectedDirectory: string;
selectedRepository?: string | null;
githubIntegrationId?: number;
Expand Down Expand Up @@ -132,6 +135,7 @@ async function trackTaskCreated(

export function useTaskCreation({
editorRef,
sessionId,
selectedDirectory,
selectedRepository,
githubIntegrationId,
Expand Down Expand Up @@ -290,22 +294,34 @@ 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 },
);

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
Expand Down Expand Up @@ -355,6 +371,7 @@ export function useTaskCreation({
canSubmit,
canSubmitBase,
editorRef,
sessionId,
selectedDirectory,
selectedRepository,
githubIntegrationId,
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/router/routes/website/new.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -21,6 +22,7 @@ function WebsiteNewTaskRoute() {
initialModel={view.initialModel}
initialMode={view.initialMode}
reportAssociation={view.reportAssociation}
suggestions={CHANNEL_TASK_SUGGESTIONS}
/>
);
}
Loading
Loading