diff --git a/packages/ui/src/features/archive/useArchiveTask.ts b/packages/ui/src/features/archive/useArchiveTask.ts index fabafdcc7b..16901cc016 100644 --- a/packages/ui/src/features/archive/useArchiveTask.ts +++ b/packages/ui/src/features/archive/useArchiveTask.ts @@ -74,7 +74,7 @@ function makeCacheWriter( function makeOrchestrationDeps( queryClient: QueryClient, keys: ArchiveCacheKeys, - options?: { skipNavigate?: boolean }, + options?: { skipNavigate?: boolean; navigateSpace?: "code" | "website" }, ): ArchiveOrchestrationDeps { const hostClient = resolveService(HOST_TRPC_CLIENT); return { @@ -91,7 +91,9 @@ function makeOrchestrationDeps( if (options?.skipNavigate) return; const view = getAppViewSnapshot(); if (view.type === "task-detail" && view.taskId === taskId) { - openTaskInput(); + openTaskInput( + options?.navigateSpace ? { space: options.navigateSpace } : undefined, + ); } }, snapshotTerminalStates: (taskId) => @@ -147,7 +149,11 @@ export async function archiveTaskImperative( taskId: string, queryClient: QueryClient, keys: ArchiveCacheKeys, - options?: { skipNavigate?: boolean; optimistic?: boolean }, + options?: { + skipNavigate?: boolean; + optimistic?: boolean; + navigateSpace?: "code" | "website"; + }, ): Promise { await archiveTask( taskId, @@ -173,7 +179,12 @@ export async function archiveTasksImperative( ); } -export function useArchiveTask() { +export function useArchiveTask(options?: { + // Which new-task screen to land on if the archived task is the active view. + // Defaults to Code; the bluebird/channels nav passes "website" so archiving + // from there returns to the website new-task screen instead. + navigateSpace?: "code" | "website"; +}) { const queryClient = useQueryClient(); const keys = useArchiveCacheKeys(); const { restore } = useUnarchiveTask(); @@ -183,6 +194,7 @@ export function useArchiveTask() { // is confirmed, rather than removing it instantly and rolling back on error. await archiveTaskImperative(taskId, queryClient, keys, { optimistic: false, + navigateSpace: options?.navigateSpace, }); const toastId = `archive-undo-${taskId}`; toast.success("Task archived", { diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 19dde93f1f..e1a5697d48 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -70,6 +70,10 @@ import { useNavigate, useRouterState } from "@tanstack/react-router"; import { type ReactNode, useEffect, useState } from "react"; import { hostClient } from "../hostClient"; +// Cap how many tasks each channel shows by default; the rest hide behind a +// "View more" button so a busy channel doesn't dominate the sidebar. +const MAX_VISIBLE_TASKS_PER_CHANNEL = 5; + // A canvas's leading icon, chosen from its template so the tree reads at a // glance: bar chart for dashboards, line chart for web-analytics, plain file for // blank canvases. @@ -376,7 +380,9 @@ function TaskRow({ const navigate = useNavigate(); const pathname = useRouterState({ select: (s) => s.location.pathname }); const { fileTask, unfileTask } = useChannelTaskMutations(); - const { archiveTask } = useArchiveTask(); + // Archiving from the bluebird/channels nav should return to the website + // new-task screen, not the Code one. + const { archiveTask } = useArchiveTask({ navigateSpace: "website" }); const taskData = useChannelTaskData(task); const workspace = useWorkspace(taskId); const workspaceMode = @@ -524,9 +530,17 @@ function ChannelSection({ const [open, setOpen] = useState(isActive); // Lifted so the hover button group stays visible while the menu is open. const [menuOpen, setMenuOpen] = useState(false); + // Only the first few tasks per channel show by default; "View more" reveals + // another batch each click so a busy channel doesn't flood the sidebar. + const [taskLimit, setTaskLimit] = useState(MAX_VISIBLE_TASKS_PER_CHANNEL); useEffect(() => { if (isActive) setOpen(true); }, [isActive]); + // Toggle expansion; collapsing also resets back to the first batch of tasks. + const toggleOpen = () => { + setOpen((o) => !o); + if (open) setTaskLimit(MAX_VISIBLE_TASKS_PER_CHANNEL); + }; // Lazy: a channel's canvases and filed tasks are only fetched once it's // expanded, so the tree doesn't fire one query per channel on mount. @@ -539,6 +553,13 @@ function ChannelSection({ ({ taskId }) => !archivedTaskIds.has(taskId) && tasks?.some((t) => t.id === taskId), ); + const displayedFiledTasks = visibleFiledTasks.slice(0, taskLimit); + const hiddenTaskCount = visibleFiledTasks.length - displayedFiledTasks.length; + // Reveal one more batch, capped at the remaining count. + const nextBatchCount = Math.min( + hiddenTaskCount, + MAX_VISIBLE_TASKS_PER_CHANNEL, + ); return ( @@ -550,7 +571,7 @@ function ChannelSection({ + )} )} diff --git a/packages/ui/src/features/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index a508276afb..0dc8e32a1d 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -10,6 +10,7 @@ import { navigateToInbox } from "@posthog/ui/router/navigationBridge"; import { useAppView } from "@posthog/ui/router/useAppView"; import { Flex, Text, Tooltip } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; +import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useConnectivity } from "../../../hooks/useConnectivity"; import { DotPatternBackground } from "../../../primitives/DotPatternBackground"; @@ -688,12 +689,10 @@ export function TaskInput({ style={{ // 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%", + // Note: this is NOT tied to `editorIsEmpty` — the input keeps its + // position as the user types so the box doesn't jump down when the + // suggestions fade out (and back in when the prompt is cleared). + top: suggestions && suggestions.length > 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" @@ -942,43 +941,53 @@ export function TaskInput({
{suggestions ? ( - suggestions.length > 0 && - editorIsEmpty && ( -
- + {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); - } - }} - /> - ))} -
-
- ) + + 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); + } + }} + /> + ))} +
+ + )} + ) : ( )}