Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit b57e1b3

Browse files
authored
refactor(ui): move the space selector into the composer's chip row
The composer's space picker becomes a chip beside the Cloud/Local workspace-mode select — same trigger shape, icon + name + caret — via a new spaceSelector slot on TaskInput (channels new-task only; /code is untouched). A space's own "+" still pre-fills that space; the sidebar's global New session now always lands on #me, with the chip as the way to retarget. Draft text survives the switch as before. Generated-By: PostHog Code Task-Id: 0331ac58-0a1c-4b4e-b884-2ff7d8e71986
1 parent 306a922 commit b57e1b3

4 files changed

Lines changed: 159 additions & 102 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
CaretDown,
3+
CheckIcon,
4+
CubeFocusIcon,
5+
LockSimpleIcon,
6+
} from "@phosphor-icons/react";
7+
import {
8+
Button,
9+
DropdownMenu,
10+
DropdownMenuContent,
11+
DropdownMenuItem,
12+
DropdownMenuTrigger,
13+
} from "@posthog/quill";
14+
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
15+
import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
16+
import { useMemo } from "react";
17+
18+
function spaceIcon(name: string | undefined) {
19+
return name === PERSONAL_CHANNEL_NAME ? (
20+
<LockSimpleIcon size={14} weight="regular" />
21+
) : (
22+
<CubeFocusIcon size={14} weight="regular" />
23+
);
24+
}
25+
26+
/**
27+
* Which space a new task files into — a chip for the composer's selector row,
28+
* drawn exactly like WorkspaceModeSelect ("Cloud"/"Local") beside it.
29+
* Personal space first, the rest alphabetical.
30+
*/
31+
export function SpaceSelect({
32+
value,
33+
onChange,
34+
}: {
35+
value: string;
36+
onChange: (channelId: string) => void;
37+
}) {
38+
const { channels } = useChannels();
39+
const current = channels.find((c) => c.id === value);
40+
41+
const options = useMemo(
42+
() =>
43+
[...channels].sort((a, b) => {
44+
if (a.name === PERSONAL_CHANNEL_NAME) return -1;
45+
if (b.name === PERSONAL_CHANNEL_NAME) return 1;
46+
return a.name.localeCompare(b.name);
47+
}),
48+
[channels],
49+
);
50+
51+
return (
52+
<DropdownMenu>
53+
<DropdownMenuTrigger
54+
render={
55+
<Button type="button" variant="outline" size="sm" aria-label="Space">
56+
<span className="text-muted-foreground">
57+
{spaceIcon(current?.name)}
58+
</span>
59+
{current?.name ?? "Space"}
60+
<CaretDown
61+
size={10}
62+
weight="bold"
63+
className="text-muted-foreground"
64+
/>
65+
</Button>
66+
}
67+
/>
68+
<DropdownMenuContent align="start" side="bottom" sideOffset={6}>
69+
{options.map((space) => (
70+
<DropdownMenuItem
71+
key={space.id}
72+
onClick={() => {
73+
if (space.id !== value) onChange(space.id);
74+
}}
75+
>
76+
<span className="text-muted-foreground">
77+
{spaceIcon(space.name)}
78+
</span>
79+
<span className="min-w-0 flex-1 truncate">{space.name}</span>
80+
{space.id === value && <CheckIcon size={14} />}
81+
</DropdownMenuItem>
82+
))}
83+
</DropdownMenuContent>
84+
</DropdownMenu>
85+
);
86+
}

‎packages/ui/src/features/canvas/components/SpacesSidebarNav.tsx‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { SpaceSection } from "@posthog/ui/features/canvas/components/SpaceSectio
1717
import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels";
1818
import { useStarredChannelSlots } from "@posthog/ui/features/canvas/hooks/useStarredChannelSlots";
1919
import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
20-
import { useCurrentChannelStore } from "@posthog/ui/features/canvas/stores/currentChannelStore";
2120
import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
2221
import { openTaskInput } from "@posthog/ui/router/useOpenTask";
2322
import { track } from "@posthog/ui/shell/analytics";
@@ -62,7 +61,6 @@ export function SpacesSidebarNav() {
6261
const toggleOnlyMyTasks = useSpacesSidebarStore((s) => s.toggleOnlyMyTasks);
6362
const spaceOrder = useSpacesSidebarStore((s) => s.spaceOrder);
6463
const setSpaceOrder = useSpacesSidebarStore((s) => s.setSpaceOrder);
65-
const currentChannelId = useCurrentChannelStore((s) => s.currentChannelId);
6664

6765
const me = pinnedSpaces.find((c) => c.name === PERSONAL_CHANNEL_NAME);
6866
// The user's drag order over the starred set; spaces they've never dragged
@@ -96,9 +94,9 @@ export function SpacesSidebarNav() {
9694
<div className="flex h-full min-h-0 flex-col">
9795
<ChannelNav />
9896

99-
{/* The create entry point, now that the floating button is gone. Files
100-
into the space you're in; the composer's space selector can retarget
101-
it. */}
97+
{/* The create entry point, now that the floating button is gone. The
98+
global button defaults to #me — the composer's space chip is where
99+
to retarget; a space's own "+" pre-fills that space instead. */}
102100
<div className="shrink-0 px-2 pb-1.5">
103101
<Button
104102
variant="outline"
@@ -108,12 +106,9 @@ export function SpacesSidebarNav() {
108106
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
109107
action_type: "new_task_open",
110108
surface: "sidebar",
111-
channel_id: currentChannelId ?? me?.id,
112-
});
113-
openTaskInput({
114-
channelId: currentChannelId ?? me?.id,
115-
space: "website",
109+
channel_id: me?.id,
116110
});
111+
openTaskInput({ channelId: me?.id, space: "website" });
117112
}}
118113
>
119114
<PlusIcon size={14} />

‎packages/ui/src/features/canvas/components/WebsiteNewTask.tsx‎

Lines changed: 52 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
1-
import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
2-
import {
3-
Button,
4-
DropdownMenu,
5-
DropdownMenuContent,
6-
DropdownMenuItem,
7-
DropdownMenuTrigger,
8-
} from "@posthog/quill";
91
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
102
import type { Task } from "@posthog/shared/domain-types";
113
import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions";
124
import { ChannelBreadcrumb } from "@posthog/ui/features/canvas/components/ChannelBreadcrumb";
135
import { ChannelContextPanel } from "@posthog/ui/features/canvas/components/ChannelContextPanel";
6+
import { SpaceSelect } from "@posthog/ui/features/canvas/components/SpaceSelect";
147
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
158
import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout";
169
import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks";
1710
import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions";
18-
import {
19-
PERSONAL_CHANNEL_NAME,
20-
useBackendChannel,
21-
} from "@posthog/ui/features/canvas/hooks/useTaskChannels";
11+
import { useBackendChannel } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
2212
import { TaskInput } from "@posthog/ui/features/task-detail/components/TaskInput";
2313
import { taskDetailQuery } from "@posthog/ui/features/tasks/queries";
2414
import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent";
@@ -120,89 +110,60 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) {
120110
[channelId, fileTask, navigate, queryClient],
121111
);
122112

123-
// The selectable spaces, personal first then alphabetical. Switching
124-
// navigates to that space's own new-task route; the composer's draft lives
125-
// in the shared "task-input" draft store, so text typed before switching
126-
// survives the navigation.
127-
const spaceOptions = useMemo(
128-
() =>
129-
[...channels].sort((a, b) => {
130-
if (a.name === PERSONAL_CHANNEL_NAME) return -1;
131-
if (b.name === PERSONAL_CHANNEL_NAME) return 1;
132-
return a.name.localeCompare(b.name);
133-
}),
134-
[channels],
113+
// Retargeting navigates to that space's own new-task route; the composer's
114+
// draft lives in the shared "task-input" draft store, so text typed before
115+
// switching survives the navigation.
116+
const handleSpaceChange = useCallback(
117+
(nextChannelId: string) => {
118+
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
119+
action_type: "new_task_open",
120+
surface: "new_task",
121+
channel_id: nextChannelId,
122+
});
123+
void navigate({
124+
to: "/website/$channelId/new",
125+
params: { channelId: nextChannelId },
126+
});
127+
},
128+
[navigate],
135129
);
136130

137131
return (
138132
<Flex className="h-full min-w-0 flex-1">
139-
<div className="flex min-w-0 flex-1 flex-col">
140-
{/* Which space the task will file into — the sidebar's New session
141-
button lands here with a default, and this is where to retarget. */}
142-
<div className="flex shrink-0 items-center gap-1.5 px-4 pt-3">
143-
<span className="text-[12px] text-muted-foreground">Space</span>
144-
<DropdownMenu>
145-
<DropdownMenuTrigger
146-
render={
147-
<Button variant="outline" size="sm" className="gap-1.5">
148-
{channelName ?? "Choose a space"}
149-
<CaretDownIcon size={12} className="text-muted-foreground" />
150-
</Button>
151-
}
152-
/>
153-
<DropdownMenuContent align="start">
154-
{spaceOptions.map((space) => (
155-
<DropdownMenuItem
156-
key={space.id}
157-
onClick={() => {
158-
if (space.id === channelId) return;
159-
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
160-
action_type: "new_task_open",
161-
surface: "new_task",
162-
channel_id: space.id,
163-
});
164-
void navigate({
165-
to: "/website/$channelId/new",
166-
params: { channelId: space.id },
167-
});
168-
}}
169-
>
170-
<span className="min-w-0 flex-1 truncate">{space.name}</span>
171-
{space.id === channelId && <CheckIcon size={14} />}
172-
</DropdownMenuItem>
173-
))}
174-
</DropdownMenuContent>
175-
</DropdownMenu>
176-
</div>
177-
<div className="min-h-0 min-w-0 flex-1">
178-
<TaskInput
179-
onTaskCreated={onTaskCreated}
180-
channelContext={channelContext}
181-
channelName={channelName}
182-
channelId={backendChannel?.id}
183-
channelContextId={channelId}
184-
allowNoRepo
185-
// So a prompt handed to openTaskInput survives routing into a channel.
186-
initialPrompt={view.initialPrompt}
187-
initialPromptKey={view.taskInputRequestId}
188-
initialCloudRepository={view.initialCloudRepository}
189-
initialModel={view.initialModel}
190-
initialMode={view.initialMode}
191-
reportAssociation={view.reportAssociation}
192-
suggestions={CHANNEL_TASK_SUGGESTIONS}
193-
onSuggestionSelect={(label) =>
194-
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
195-
action_type: "new_task_suggestion",
196-
surface: "new_task",
197-
channel_id: channelId,
198-
suggestion_label: label,
199-
})
200-
}
201-
onContextChipClick={
202-
channelContext ? handleContextChipClick : undefined
203-
}
204-
/>
205-
</div>
133+
<div className="min-w-0 flex-1">
134+
<TaskInput
135+
// Beside the Cloud/Local chip: which space the task files into.
136+
// Arriving from a space's own "+" this is pre-filled; the sidebar's
137+
// global New session lands on #me.
138+
spaceSelector={
139+
<SpaceSelect value={channelId} onChange={handleSpaceChange} />
140+
}
141+
onTaskCreated={onTaskCreated}
142+
channelContext={channelContext}
143+
channelName={channelName}
144+
channelId={backendChannel?.id}
145+
channelContextId={channelId}
146+
allowNoRepo
147+
// So a prompt handed to openTaskInput survives routing into a channel.
148+
initialPrompt={view.initialPrompt}
149+
initialPromptKey={view.taskInputRequestId}
150+
initialCloudRepository={view.initialCloudRepository}
151+
initialModel={view.initialModel}
152+
initialMode={view.initialMode}
153+
reportAssociation={view.reportAssociation}
154+
suggestions={CHANNEL_TASK_SUGGESTIONS}
155+
onSuggestionSelect={(label) =>
156+
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
157+
action_type: "new_task_suggestion",
158+
surface: "new_task",
159+
channel_id: channelId,
160+
suggestion_label: label,
161+
})
162+
}
163+
onContextChipClick={
164+
channelContext ? handleContextChipClick : undefined
165+
}
166+
/>
206167
</div>
207168
<ResizableSidebar
208169
open={contextPanelOpen && !!channelContext}

‎packages/ui/src/features/task-detail/components/TaskInput.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ import { track } from "@posthog/ui/shell/analytics";
2424
import { Box, Flex, Text, Tooltip } from "@radix-ui/themes";
2525
import { useQuery } from "@tanstack/react-query";
2626
import { AnimatePresence, motion } from "framer-motion";
27-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
27+
import {
28+
type ReactNode,
29+
useCallback,
30+
useEffect,
31+
useMemo,
32+
useRef,
33+
useState,
34+
} from "react";
2835
import { useConnectivity } from "../../../hooks/useConnectivity";
2936
import { DotPatternBackground } from "../../../primitives/DotPatternBackground";
3037
import { toast } from "../../../primitives/toast";
@@ -151,6 +158,12 @@ interface TaskInputProps {
151158
* the chip is non-interactive (only dismissable).
152159
*/
153160
onContextChipClick?: () => void;
161+
/**
162+
* A space picker chip rendered first in the selector row above the composer
163+
* (beside the workspace-mode chip). Channels new-task screen only — /code
164+
* has no spaces to pick.
165+
*/
166+
spaceSelector?: ReactNode;
154167
}
155168

156169
export function TaskInput({
@@ -170,6 +183,7 @@ export function TaskInput({
170183
suggestions,
171184
onSuggestionSelect,
172185
onContextChipClick,
186+
spaceSelector,
173187
}: TaskInputProps = {}) {
174188
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
175189
const trpc = useHostTRPC();
@@ -1204,6 +1218,7 @@ export function TaskInput({
12041218
align="center"
12051219
className="absolute bottom-full left-0 mb-2 min-w-0"
12061220
>
1221+
{spaceSelector}
12071222
{piHarnessEnabled && (
12081223
<AgentRuntimeSelect
12091224
value={runtime}

0 commit comments

Comments
 (0)