From 72e327ad88bd48a6d2141b8a6c2cc766a322f084 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Fri, 10 Jul 2026 21:40:13 +0100 Subject: [PATCH 1/2] feat(channels): default-file channel-less tasks into #me Tasks created without a channel (the /code composer, /website/new, command center) now get filed to the private #me channel behind the project-bluebird flag, so they still surface in the Channels space instead of staying unfiled. Filing is best-effort; on failure the task stays unfiled as before. Generated-By: PostHog Code Task-Id: a64c4ae7-96e3-4284-907f-e23dd0fcd500 --- packages/shared/src/analytics-events.ts | 1 + .../task-detail/hooks/useTaskCreation.ts | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index 67eb146a7e..7a26c7408c 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -831,6 +831,7 @@ export type ChannelsSurface = | "sidebar" | "command_menu" | "new_task" + | "task_input" | "channel_home" | "channel_history" | "channel_artifacts" diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 91acc634e1..f25b34307a 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -13,10 +13,16 @@ import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react"; import { type Adapter, ANALYTICS_EVENTS, + PROJECT_BLUEBIRD_FLAG, type TaskCreationInput, type WorkspaceMode, } from "@posthog/shared"; import type { ExecutionMode, Task } from "@posthog/shared/domain-types"; +import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; +import { useChannelMutations } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; +import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; +import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { useTaskInputPrefillStore } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore"; import { navigateToTaskPending } from "@posthog/ui/router/navigationBridge"; import { openTask, openTaskInput } from "@posthog/ui/router/useOpenTask"; @@ -201,6 +207,54 @@ export function useTaskCreation({ // Used to name the task occupying a branch's worktree when reuse is blocked. const { data: tasks } = useTasks(); + // Tasks created without a channel default into the private #me channel so + // they still surface in the Channels space instead of staying unfiled. + const bluebirdEnabled = useFeatureFlag( + PROJECT_BLUEBIRD_FLAG, + import.meta.env.DEV, + ); + const apiClient = useOptionalAuthenticatedClient(); + const { createChannel } = useChannelMutations(); + const { fileTask } = useChannelTaskMutations(); + + const fileToPersonalChannel = useCallback( + async (task: Task) => { + if (!apiClient) return; + let meFolderId: string | undefined; + try { + // List fresh rather than trusting the polled channels cache — a stale + // miss here would create a duplicate "me" folder. + const rows = await apiClient.getDesktopFileSystemChannels(); + const existing = rows.find( + (fs) => + fs.type === "folder" && + fs.path.replace(/^\/+/, "") === PERSONAL_CHANNEL_NAME, + ); + meFolderId = + existing?.id ?? (await createChannel(PERSONAL_CHANNEL_NAME)).id; + await fileTask(meFolderId, task.id, task.title); + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "file_task", + surface: "task_input", + channel_id: meFolderId, + task_id: task.id, + success: true, + }); + } catch (error) { + // Best-effort: on failure the task just stays unfiled, as before. + log.warn("Failed to default-file task to #me", { error }); + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "file_task", + surface: "task_input", + channel_id: meFolderId, + task_id: task.id, + success: false, + }); + } + }, + [apiClient, createChannel, fileTask], + ); + const hasRequiredPath = allowNoRepo ? true : workspaceMode === "cloud" @@ -376,6 +430,9 @@ export function useTaskCreation({ if (!pendingTaskKey && !contentOverride) { editor.clear(); } + if (bluebirdEnabled && !channelId && !channelName) { + void fileToPersonalChannel(output.task); + } onTaskCreatedEffect?.(output.task); if (onTaskCreated) { onTaskCreated(output.task); @@ -493,6 +550,8 @@ export function useTaskCreation({ channelName, channelId, allowNoRepo, + bluebirdEnabled, + fileToPersonalChannel, clearTaskInputReportAssociation, invalidateTasks, onTaskCreated, From 7b8bb1fcaf072f3809800f3745e0f460127580a8 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Fri, 10 Jul 2026 21:40:15 +0100 Subject: [PATCH 2/2] fix(channels): default to per-user backend #me channel, not shared folder Review feedback: the desktop file system is project-scoped, so filing into a "me" folder shared tasks across teammates and could create duplicate folders under concurrent creation. Set the per-user backend personal channel (provisioned lazily and idempotently server-side) as the task's channel at creation time instead. Generated-By: PostHog Code Task-Id: a64c4ae7-96e3-4284-907f-e23dd0fcd500 --- .../features/canvas/hooks/useTaskChannels.ts | 7 +- .../task-detail/hooks/useTaskCreation.ts | 73 ++++++------------- 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/packages/ui/src/features/canvas/hooks/useTaskChannels.ts b/packages/ui/src/features/canvas/hooks/useTaskChannels.ts index 944dab81de..f0a3068673 100644 --- a/packages/ui/src/features/canvas/hooks/useTaskChannels.ts +++ b/packages/ui/src/features/canvas/hooks/useTaskChannels.ts @@ -20,7 +20,7 @@ export function normalizeChannelName(name: string): string { * folder "channels" stay on the desktop file system for CONTEXT.md and * artifacts). Listing also lazily provisions the requester's #me channel. */ -export function useTaskChannels(): { +export function useTaskChannels(options?: { enabled?: boolean }): { channels: TaskChannel[]; personalChannel: TaskChannel | undefined; isLoading: boolean; @@ -28,7 +28,10 @@ export function useTaskChannels(): { const query = useAuthenticatedQuery( TASK_CHANNELS_QUERY_KEY, (client) => client.getTaskChannels(), - { refetchInterval: TASK_CHANNELS_POLL_INTERVAL_MS }, + { + enabled: options?.enabled ?? true, + refetchInterval: TASK_CHANNELS_POLL_INTERVAL_MS, + }, ); const channels = useMemo(() => query.data ?? [], [query.data]); const personalChannel = useMemo( diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index f25b34307a..36c8024c1f 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -18,10 +18,7 @@ import { type WorkspaceMode, } from "@posthog/shared"; import type { ExecutionMode, Task } from "@posthog/shared/domain-types"; -import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; -import { useChannelMutations } from "@posthog/ui/features/canvas/hooks/useChannels"; -import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; -import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; +import { useTaskChannels } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { useTaskInputPrefillStore } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore"; import { navigateToTaskPending } from "@posthog/ui/router/navigationBridge"; @@ -207,53 +204,16 @@ export function useTaskCreation({ // Used to name the task occupying a branch's worktree when reuse is blocked. const { data: tasks } = useTasks(); - // Tasks created without a channel default into the private #me channel so - // they still surface in the Channels space instead of staying unfiled. + // Tasks created without a channel default into the user's private #me + // backend channel so they still surface in the Channels space instead of + // staying unfiled. The personal channel is per-user and provisioned lazily + // server-side on first list, so this can't collide across teammates. If it + // hasn't loaded yet the task is created unfiled, as before. const bluebirdEnabled = useFeatureFlag( PROJECT_BLUEBIRD_FLAG, import.meta.env.DEV, ); - const apiClient = useOptionalAuthenticatedClient(); - const { createChannel } = useChannelMutations(); - const { fileTask } = useChannelTaskMutations(); - - const fileToPersonalChannel = useCallback( - async (task: Task) => { - if (!apiClient) return; - let meFolderId: string | undefined; - try { - // List fresh rather than trusting the polled channels cache — a stale - // miss here would create a duplicate "me" folder. - const rows = await apiClient.getDesktopFileSystemChannels(); - const existing = rows.find( - (fs) => - fs.type === "folder" && - fs.path.replace(/^\/+/, "") === PERSONAL_CHANNEL_NAME, - ); - meFolderId = - existing?.id ?? (await createChannel(PERSONAL_CHANNEL_NAME)).id; - await fileTask(meFolderId, task.id, task.title); - track(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: "file_task", - surface: "task_input", - channel_id: meFolderId, - task_id: task.id, - success: true, - }); - } catch (error) { - // Best-effort: on failure the task just stays unfiled, as before. - log.warn("Failed to default-file task to #me", { error }); - track(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: "file_task", - surface: "task_input", - channel_id: meFolderId, - task_id: task.id, - success: false, - }); - } - }, - [apiClient, createChannel, fileTask], - ); + const { personalChannel } = useTaskChannels({ enabled: bluebirdEnabled }); const hasRequiredPath = allowNoRepo ? true @@ -361,6 +321,11 @@ export function useTaskCreation({ } const settings = useSettingsStore.getState(); + const defaultedChannelId = + bluebirdEnabled && !channelId && !channelName + ? personalChannel?.id + : undefined; + const input = prepareTaskInput(serializedContent, filePaths, { // In channels chat-box mode no repo is attached up front, even if a // directory/repo is lingering in the persisted picker state. @@ -383,7 +348,7 @@ export function useTaskCreation({ additionalDirectories, channelContext, channelName, - channelId, + channelId: channelId ?? defaultedChannelId, customInstructions: getEffectiveCustomInstructions(settings), autoPublishCloudRuns: settings.autoPublishCloudRuns, rtkEnabledCloud: settings.rtkEnabledCloud, @@ -430,8 +395,14 @@ export function useTaskCreation({ if (!pendingTaskKey && !contentOverride) { editor.clear(); } - if (bluebirdEnabled && !channelId && !channelName) { - void fileToPersonalChannel(output.task); + if (defaultedChannelId) { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "file_task", + surface: "task_input", + channel_id: defaultedChannelId, + task_id: output.task.id, + success: true, + }); } onTaskCreatedEffect?.(output.task); if (onTaskCreated) { @@ -551,7 +522,7 @@ export function useTaskCreation({ channelId, allowNoRepo, bluebirdEnabled, - fileToPersonalChannel, + personalChannel?.id, clearTaskInputReportAssociation, invalidateTasks, onTaskCreated,