From 0e975d52e9871095729a08205e0cfc1a8a1cb9e9 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 19 Jun 2026 14:58:19 -0700 Subject: [PATCH 1/5] feat(channels): starter prompt suggestions on the new-task screen Add a 2x4 grid of starter-prompt cards below the input on the project-bluebird channels new-task screen. Each card mirrors the SuggestedTaskCard look (icon badge + title + description); clicking one fills the composer with a multi-line template that ends in a "User input:" block of fill-in lines. - New CHANNEL_TASK_SUGGESTIONS data + SuggestedPromptCard component - TaskInput renders the channel suggestions OR SuggestedTasksPanel (never both), so the codebase-discovery/SDK-health cards don't show on channels - Raise the input block when suggestions are present so the longer list isn't squished; extra gap above the suggestions - Fill via setPendingContent so template line breaks survive - Fix: clear the editor draft before onTaskCreated navigates, so a submitted prompt doesn't persist into the next new task Generated-By: PostHog Code Task-Id: 56abea18-4f33-45cb-ae43-55182b861d46 --- .../features/canvas/channelTaskSuggestions.ts | 85 +++++++++++++++++++ .../canvas/components/WebsiteNewTask.tsx | 2 + .../components/SuggestedPromptCard.tsx | 63 ++++++++++++++ .../task-detail/components/TaskInput.tsx | 52 +++++++++++- .../task-detail/hooks/useTaskCreation.ts | 10 ++- packages/ui/src/router/routes/website/new.tsx | 2 + 6 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/features/canvas/channelTaskSuggestions.ts create mode 100644 packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx diff --git a/packages/ui/src/features/canvas/channelTaskSuggestions.ts b/packages/ui/src/features/canvas/channelTaskSuggestions.ts new file mode 100644 index 0000000000..e7684d8729 --- /dev/null +++ b/packages/ui/src/features/canvas/channelTaskSuggestions.ts @@ -0,0 +1,85 @@ +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", + 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", + 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", + 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", + 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", + 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", + 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", + 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", + 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..6729f0d3b2 --- /dev/null +++ b/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx @@ -0,0 +1,63 @@ +import type { Icon } from "@phosphor-icons/react"; +import { Flex, Text } from "@radix-ui/themes"; + +export interface SuggestedPrompt { + label: string; + description: string; + prompt: string; + icon: Icon; + color: string; +} + +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..4f47a041d0 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(); @@ -674,7 +685,9 @@ export function TaskInput({
0 ? "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 +935,42 @@ 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 }, + ], + }); + }} + /> + ))} +
+
+ ) + ) : ( + + )}
diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 8e5f1aa48c..cabdfdcf06 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -290,15 +290,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 }, 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} /> ); } From 02fa1aafec2dd80f2b812de567eafb408f0e1ab8 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 19 Jun 2026 15:06:02 -0700 Subject: [PATCH 2/5] fix(task-input): clear draft on success regardless of onTaskReady editor.clear() only ran inside the onTaskReady callback; for repo-less channel tasks the callback can navigate/unmount the editor before the persisted draft is wiped, so a submitted prompt reappeared on the next new task. Clear drafts[sessionId] directly in the guaranteed result.success block, which always runs and survives the unmount. Generated-By: PostHog Code Task-Id: 56abea18-4f33-45cb-ae43-55182b861d46 --- .../features/task-detail/components/TaskInput.tsx | 1 + .../features/task-detail/hooks/useTaskCreation.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 4f47a041d0..19ca732037 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -543,6 +543,7 @@ export function TaskInput({ setAdditionalDirectories, } = useTaskCreation({ editorRef, + sessionId, selectedDirectory, selectedRepository: selectedCloudRepository, githubUserIntegrationId: selectedGithubUserIntegrationId, diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index cabdfdcf06..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, @@ -310,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 @@ -359,6 +371,7 @@ export function useTaskCreation({ canSubmit, canSubmitBase, editorRef, + sessionId, selectedDirectory, selectedRepository, githubIntegrationId, From 9e84d101c1c1169766627de9a314e4621e8c13fb Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 19 Jun 2026 15:24:06 -0700 Subject: [PATCH 3/5] feat(channels): set task mode per suggestion "Fix a bug" and "Build a new feature" start in plan mode; the analysis suggestions start in auto mode. Selecting a card applies its mode via setConfigOption (guarded by isValidConfigValue, mirroring initialMode). Generated-By: PostHog Code Task-Id: 56abea18-4f33-45cb-ae43-55182b861d46 --- packages/ui/src/features/canvas/channelTaskSuggestions.ts | 8 ++++++++ .../task-detail/components/SuggestedPromptCard.tsx | 3 +++ .../ui/src/features/task-detail/components/TaskInput.tsx | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/packages/ui/src/features/canvas/channelTaskSuggestions.ts b/packages/ui/src/features/canvas/channelTaskSuggestions.ts index e7684d8729..5529b1447e 100644 --- a/packages/ui/src/features/canvas/channelTaskSuggestions.ts +++ b/packages/ui/src/features/canvas/channelTaskSuggestions.ts @@ -23,6 +23,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -31,6 +32,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -39,6 +41,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -47,6 +50,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -55,6 +59,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -63,6 +68,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -71,6 +77,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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):", }, @@ -79,6 +86,7 @@ export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ 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/task-detail/components/SuggestedPromptCard.tsx b/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx index 6729f0d3b2..b1b36e9ab2 100644 --- a/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx +++ b/packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx @@ -1,4 +1,5 @@ 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 { @@ -7,6 +8,8 @@ export interface SuggestedPrompt { prompt: string; icon: Icon; color: string; + /** Task mode to apply when this suggestion is selected. */ + mode: ExecutionMode; } export interface SuggestedPromptCardProps { diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 19ca732037..72019e975c 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -963,6 +963,11 @@ export function TaskInput({ { 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); + } }} /> ))} From 58595d64de65cd32a344c97f5e2f90cfa5407177 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 19 Jun 2026 16:12:30 -0700 Subject: [PATCH 4/5] fix(task-input): recenter input when suggestion cards hide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The input was raised to 38% whenever suggestions were provided, but the cards only render while the editor is empty — so once the user typed, the cards vanished yet the input stayed raised. Gate the position on the same editorIsEmpty condition so it recenters to 50% as the user types. Generated-By: PostHog Code Task-Id: 56abea18-4f33-45cb-ae43-55182b861d46 --- .../ui/src/features/task-detail/components/TaskInput.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 72019e975c..a508276afb 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -686,9 +686,14 @@ export function TaskInput({
0 ? "38%" : "50%", + // 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" From 03be99aa72151436ef90368d0113bce1ef8350f0 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 19 Jun 2026 17:04:18 -0700 Subject: [PATCH 5/5] fix(channels): reuse local folders, stop git-init prompt on scratch tasks Follow-up to #2735 (generic chat box with lazy repo attach). Two local-mode regressions: 1. The channel system prompt sent the agent straight to list_repos/clone_repo, cloning from remote even when the user already has the repo checked out locally. AgentService now fetches the user's previously-used local folders and embeds them in the channel prompt, with guidance to reuse a local match (or ask the user for a path) first and only clone from remote as a last resort, after confirming. 2. Opening a repo-less channel task popped the native "initialize git?" dialog: the synthetic scratch workspace has folderId "" (falsy), so the navigation task binder's guard missed and it registered the empty scratch dir as a folder. Mark scratch workspaces with isScratch and short-circuit so they are never registered or git-init'd. The dialog now only fires when a real folder is selected for a coding task. Generated-By: PostHog Code Task-Id: d7d5491e-f66a-4848-9620-98006834f3d6 --- packages/shared/src/workspace-domain.ts | 6 ++++ .../src/features/navigation/taskBinderImpl.ts | 8 +++++ .../src/services/agent/agent.test.ts | 4 +++ .../src/services/agent/agent.ts | 36 ++++++++++++++++--- .../src/services/workspace/workspace.ts | 1 + .../workspace/workspace.verify.test.ts | 4 +++ 6 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/workspace-domain.ts b/packages/shared/src/workspace-domain.ts index 4235be5f46..5538b0a1db 100644 --- a/packages/shared/src/workspace-domain.ts +++ b/packages/shared/src/workspace-domain.ts @@ -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; diff --git a/packages/ui/src/features/navigation/taskBinderImpl.ts b/packages/ui/src/features/navigation/taskBinderImpl.ts index 3d08df40d8..a3ba46c5e1 100644 --- a/packages/ui/src/features/navigation/taskBinderImpl.ts +++ b/packages/ui/src/features/navigation/taskBinderImpl.ts @@ -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); diff --git a/packages/workspace-server/src/services/agent/agent.test.ts b/packages/workspace-server/src/services/agent/agent.test.ts index 40c713a625..e5f711ca0a 100644 --- a/packages/workspace-server/src/services/agent/agent.test.ts +++ b/packages/workspace-server/src/services/agent/agent.test.ts @@ -176,6 +176,9 @@ function createMockDependencies() { workspaceSettings: { getWorktreeLocation: () => "/mock/worktrees", }, + foldersService: { + getFolders: vi.fn().mockResolvedValue([]), + }, loggerFactory: { scope: () => ({ info: vi.fn(), @@ -220,6 +223,7 @@ describe("AgentService", () => { deps.storagePaths as never, deps.workspaceRepository as never, deps.workspaceSettings as never, + deps.foldersService as never, deps.loggerFactory as never, ); vi.spyOn(service, "emit"); diff --git a/packages/workspace-server/src/services/agent/agent.ts b/packages/workspace-server/src/services/agent/agent.ts index 593981a00d..ebf83a9d3a 100644 --- a/packages/workspace-server/src/services/agent/agent.ts +++ b/packages/workspace-server/src/services/agent/agent.ts @@ -66,6 +66,9 @@ import { import { inject, injectable, preDestroy } from "inversify"; import { WORKSPACE_REPOSITORY } from "../../db/identifiers"; import type { IWorkspaceRepository } from "../../db/repositories/workspace-repository"; +import type { FoldersService } from "../folders/folders"; +import { FOLDERS_SERVICE } from "../folders/identifiers"; +import type { RegisteredFolder } from "../folders/schemas"; import { POSTHOG_PLUGIN_SERVICE } from "../posthog-plugin/identifiers"; import type { PosthogPluginService } from "../posthog-plugin/posthog-plugin"; import { PROCESS_TRACKING_SERVICE } from "../process-tracking/identifiers"; @@ -372,6 +375,8 @@ export class AgentService extends TypedEventEmitter { private readonly workspaceRepository: IWorkspaceRepository, @inject(WORKSPACE_SETTINGS_SERVICE) private readonly workspaceSettings: IWorkspaceSettings, + @inject(FOLDERS_SERVICE) + private readonly foldersService: FoldersService, @inject(AGENT_LOGGER) loggerFactory: AgentLogger, ) { @@ -535,6 +540,7 @@ export class AgentService extends TypedEventEmitter { additionalDirectories?: string[], systemPromptOverride?: string, channelMode?: boolean, + knownLocalFolders?: RegisteredFolder[], ): { append: string; } { @@ -575,16 +581,30 @@ When creating pull requests, add the following footer at the end of the PR descr \`\`\``; if (channelMode) { + const localFolders = (knownLocalFolders ?? []).filter( + (f) => f.exists !== false, + ); + const localFoldersBlock = localFolders.length + ? `\n\nThe user already has these repositories checked out locally on this machine. Prefer reusing one of these over cloning anything:\n${localFolders + .map( + (f) => + ` - ${f.name} — ${f.path}${f.remoteUrl ? ` (${f.remoteUrl})` : ""}`, + ) + .join("\n")}` + : ""; + prompt += ` ## Channel task (no repository attached) You are running in a PostHog channel as a general-purpose assistant. This task may NOT need a code repository at all — it could be data analysis via PostHog tools, drafting a message, or answering a question. Do not assume you need a repo. - Your working directory is a scratch directory, not a git checkout. Treat it as empty. -- Decide from the user's request (and the channel CONTEXT.md included above, if any) whether the task actually requires working inside a code repository. -- Only if a repository is genuinely required: pick which one from the request and CONTEXT.md. Repositories named in CONTEXT.md are the most likely candidates — prefer them. Call \`list_repos\` to see what is available. -- Bring a repo into your workspace with the \`clone_repo\` tool (pass \`owner/repo\`). It clones into a subdirectory of your working directory and returns the path — cd into that path for all git work. -- If a repository is required but you cannot confidently determine which one, use the AskUserQuestion tool to ask the user to choose before cloning. Do not guess.`; +- Decide from the user's request (and the channel CONTEXT.md included above, if any) whether the task actually requires working inside a code repository. If it doesn't, just do the work in the scratch directory — do NOT attach a repo. + +If a repository IS genuinely required, attach one in this priority order: +1. **Reuse a folder the user already has locally.** ${localFolders.length ? "Pick the one that best matches the request and the channel CONTEXT.md, then `cd` into its absolute path and do all git and file work there. It is already on disk — do NOT clone it again." : "If the user names a folder or path, `cd` into that absolute path and work there."} +2. **If you can't confidently pick one** (none clearly match, or it's ambiguous), use the AskUserQuestion tool to ask the user which local folder to use, or for the path where the folder lives on this machine. Do not guess. +3. **Only as a last resort** — when the user has no local copy, or explicitly wants a fresh checkout — clone from remote. Call \`list_repos\` to see what's available (prefer repos named in CONTEXT.md), then **confirm with the user via AskUserQuestion before cloning**, and use \`clone_repo\` (pass \`owner/repo\`); it clones into a subdirectory of your working directory and returns the path to \`cd\` into.${localFoldersBlock}`; } if (customInstructions) { @@ -660,6 +680,13 @@ You are running in a PostHog channel as a general-purpose assistant. This task m this.workspaceSettings.getWorktreeLocation(), ); + // In channel mode the agent decides at runtime whether it needs a repo. Give + // it the user's previously-used local folders so it can reuse one (or ask) + // instead of cloning from remote. Only fetched for channel sessions. + const knownLocalFolders = channelMode + ? await this.foldersService.getFolders().catch(() => []) + : []; + const additionalDirectories = taskId === "__preview__" ? [] @@ -717,6 +744,7 @@ You are running in a PostHog channel as a general-purpose assistant. This task m additionalDirectories, systemPromptOverride, channelMode, + knownLocalFolders, ); const bundledSkillsDir = join( diff --git a/packages/workspace-server/src/services/workspace/workspace.ts b/packages/workspace-server/src/services/workspace/workspace.ts index c21277fec8..df3d844c58 100644 --- a/packages/workspace-server/src/services/workspace/workspace.ts +++ b/packages/workspace-server/src/services/workspace/workspace.ts @@ -898,6 +898,7 @@ export class WorkspaceService extends TypedEventEmitter baseBranch: null, linkedBranch: null, createdAt: new Date().toISOString(), + isScratch: true, }; } diff --git a/packages/workspace-server/src/services/workspace/workspace.verify.test.ts b/packages/workspace-server/src/services/workspace/workspace.verify.test.ts index 0a086feb41..e287f2ad31 100644 --- a/packages/workspace-server/src/services/workspace/workspace.verify.test.ts +++ b/packages/workspace-server/src/services/workspace/workspace.verify.test.ts @@ -166,10 +166,14 @@ describe("WorkspaceService.verifyWorkspaceExists", () => { mode: "local", folderPath: scratchPath, worktreePath: null, + // Marked so the navigation task binder skips folder registration (and the + // "initialize git" dialog) for repo-less channel tasks. + isScratch: true, }); const all = await service.getAllWorkspaces(); expect(all[TASK_ID]?.folderPath).toBe(scratchPath); + expect(all[TASK_ID]?.isScratch).toBe(true); // It is not backed by a DB row. expect(workspaceRepo.findByTaskId(TASK_ID)).toBeNull();