Skip to content
Merged
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
20 changes: 16 additions & 4 deletions packages/ui/src/features/archive/useArchiveTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostTrpcClient>(HOST_TRPC_CLIENT);
return {
Expand All @@ -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) =>
Expand Down Expand Up @@ -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<void> {
await archiveTask(
taskId,
Expand All @@ -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();
Expand All @@ -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", {
Expand Down
42 changes: 39 additions & 3 deletions packages/ui/src/features/canvas/components/ChannelsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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.
Expand All @@ -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 (
<Box className="group/chan relative">
Expand All @@ -550,7 +571,7 @@ function ChannelSection({
<Button
variant="default"
size="default"
onClick={() => setOpen((o) => !o)}
onClick={toggleOpen}
aria-expanded={open}
className="w-full min-w-0 flex-1 justify-start gap-2 aria-expanded:bg-transparent"
>
Expand Down Expand Up @@ -618,7 +639,7 @@ function ChannelSection({
active={pathname === `${base}/dashboards/${d.id}`}
/>
))}
{visibleFiledTasks.map(({ id: channelTaskId, taskId }) => {
{displayedFiledTasks.map(({ id: channelTaskId, taskId }) => {
const task = tasks?.find((t) => t.id === taskId);
const title = task?.title || "Untitled task";
return (
Expand All @@ -640,6 +661,21 @@ function ChannelSection({
/>
);
})}
{hiddenTaskCount > 0 && (
<Button
variant="default"
size="default"
onClick={() =>
setTaskLimit((n) => n + MAX_VISIBLE_TASKS_PER_CHANNEL)
}
className="w-full min-w-0 justify-start gap-2 text-[13px] text-gray-10"
>
<span className="inline-flex size-[14px] shrink-0 items-center justify-center">
<CaretDownIcon size={12} />
</span>
View {nextBatchCount} more
</Button>
)}
</Flex>
)}
</Box>
Expand Down
93 changes: 51 additions & 42 deletions packages/ui/src/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -942,43 +941,53 @@ export function TaskInput({
</Flex>
<div className="absolute top-full right-0 left-0 z-10">
{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)"
<AnimatePresence>
{suggestions.length > 0 && editorIsEmpty && (
<motion.div
key="suggestions"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="mt-6 flex flex-col gap-2"
>
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>
)
<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>
</motion.div>
)}
</AnimatePresence>
) : (
<SuggestedTasksPanel />
)}
Expand Down
Loading