From 202f240857a37bf6f03671513a909cfd99ea5174 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 4 Aug 2026 15:57:35 +0100 Subject: [PATCH 1/5] clean up slash command menu, slash command in tip tap just text --- .../canvas/components/ChannelHomeComposer.tsx | 3 +- .../canvas/components/SpaceSelect.tsx | 136 ++++++++++++++++++ .../canvas/components/WebsiteNewTask.tsx | 25 ++++ .../components/AttachmentMenu.tsx | 32 ++++- .../components/PromptInput.stories.tsx | 2 +- .../message-editor/components/PromptInput.tsx | 68 +++++++-- .../message-editor/tiptap/MentionChipView.tsx | 24 +++- .../message-editor/tiptap/SuggestionList.tsx | 51 ++++--- .../message-editor/tiptap/useTiptapEditor.ts | 11 ++ .../components/ChannelContextChip.tsx | 52 +++++-- .../task-detail/components/TaskInput.tsx | 33 +++-- .../components/WorkspaceModeSelect.tsx | 16 +-- 12 files changed, 370 insertions(+), 83 deletions(-) create mode 100644 products/desktop/packages/ui/src/features/canvas/components/SpaceSelect.tsx diff --git a/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index 8a630044a584..0da429abc2ba 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -362,7 +362,6 @@ export const ChannelHomeComposer = forwardRef< [sessionId, modeOption, setConfigOption], ); - const hints = ["@ to add files", "/ for skills"].join(", "); const isBusy = isCreatingTask || isStartingCanvas; const submitComposer = canvasArmed ? handleCanvasSubmit : submit; @@ -394,7 +393,7 @@ export const ChannelHomeComposer = forwardRef< placeholder={ canvasArmed ? "Describe the canvas to build — the agent generates and publishes it" - : `What do you want to ship? ${hints}` + : `What do you want to ship?` } editorHeight="large" disabled={isBusy} diff --git a/products/desktop/packages/ui/src/features/canvas/components/SpaceSelect.tsx b/products/desktop/packages/ui/src/features/canvas/components/SpaceSelect.tsx new file mode 100644 index 000000000000..e936001c681f --- /dev/null +++ b/products/desktop/packages/ui/src/features/canvas/components/SpaceSelect.tsx @@ -0,0 +1,136 @@ +import { + Button, + Combobox, + ComboboxCollection, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxInput, + ComboboxItem, + ComboboxLabel, + ComboboxList, + ComboboxSeparator, + ComboboxTrigger, +} from "@posthog/quill"; +import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph"; +import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; +import { useMemo, useRef } from "react"; + +interface SpaceGroup { + value: string; + items: string[]; +} + +/** + * Which space a new task files into — a chip for the composer's selector row, + * drawn like the EnvironmentSelector and WorkspaceModeSelect beside it. A + * project can carry hundreds of spaces, so the list is searchable: starred + * spaces (with #me leading) sit above the rest, under the same "Starred" / + * "Spaces" headings the sidebar list uses. + */ +export function SpaceSelect({ + value, + onChange, +}: { + value: string; + onChange: (channelId: string) => void; +}) { + const { channels } = useChannels(); + const anchorRef = useRef(null); + const current = channels.find((c) => c.id === value) ?? null; + + const byId = useMemo( + () => new Map(channels.map((c) => [c.id, c])), + [channels], + ); + + // Ids, not Channel objects: the channels query repolls and rebuilds its + // objects, so a selected object stops matching the list by identity and the + // combobox silently drops the selection. Ids compare by value. + // + // `useChannels` already sorts by name, so both groups stay alphabetical + // without re-sorting; #me leads because it's where an unfiled task goes. + // An empty group is dropped rather than rendered as a bare heading. + const groups = useMemo(() => { + const starred = channels.filter( + (c) => c.channelType === "personal" || c.starred, + ); + const rest = channels.filter( + (c) => c.channelType !== "personal" && !c.starred, + ); + return [ + { value: "Starred", items: starred.map((c) => c.id) }, + { value: "Spaces", items: rest.map((c) => c.id) }, + ].filter((group) => group.items.length > 0); + }, [channels]); + + const triggerGlyph = channelGlyph(current?.name, { size: 14, space: true }); + + return ( + + items={groups} + value={value} + onValueChange={(nextId) => { + if (nextId && nextId !== value) onChange(nextId); + }} + itemToStringLabel={(id) => byId.get(id)?.name ?? ""} + > +
+ + {triggerGlyph && ( + + {triggerGlyph} + + )} + + {current?.name ?? "Space"} + + + } + /> +
+ + + No spaces found. + + {(group: SpaceGroup, index: number) => ( + + {group.value} + + {(id: string) => { + const space = byId.get(id); + if (!space) return null; + return ( + + {channelGlyph(space.name, { size: 14, space: true })} + {space.name} + + ); + }} + + {index < groups.length - 1 && } + + )} + + + + ); +} diff --git a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx index bc2b4b4da90c..c24418c17935 100644 --- a/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx +++ b/products/desktop/packages/ui/src/features/canvas/components/WebsiteNewTask.tsx @@ -3,6 +3,7 @@ import type { Task } from "@posthog/shared/domain-types"; import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions"; import { ChannelBreadcrumb } from "@posthog/ui/features/canvas/components/ChannelBreadcrumb"; import { ChannelContextPanel } from "@posthog/ui/features/canvas/components/ChannelContextPanel"; +import { SpaceSelect } from "@posthog/ui/features/canvas/components/SpaceSelect"; import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; import { useChannelsLayout } from "@posthog/ui/features/canvas/hooks/useChannelsLayout"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; @@ -106,10 +107,34 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) { [channelId, fileTask, navigate, queryClient], ); + // Retargeting navigates to that space's own new-task route; the composer's + // draft lives in the shared "task-input" draft store, so text typed before + // switching survives the navigation. + const handleSpaceChange = useCallback( + (nextChannelId: string) => { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "new_task_open", + surface: "new_task", + channel_id: nextChannelId, + }); + void navigate({ + to: "/website/$channelId/new", + params: { channelId: nextChannelId }, + }); + }, + [navigate], + ); + return (
+ } onTaskCreated={onTaskCreated} channelContext={channelContext} channelName={channelName} diff --git a/products/desktop/packages/ui/src/features/message-editor/components/AttachmentMenu.tsx b/products/desktop/packages/ui/src/features/message-editor/components/AttachmentMenu.tsx index d7d291a0d8d5..26773dd6bebb 100644 --- a/products/desktop/packages/ui/src/features/message-editor/components/AttachmentMenu.tsx +++ b/products/desktop/packages/ui/src/features/message-editor/components/AttachmentMenu.tsx @@ -15,6 +15,7 @@ import { isRasterImageFile } from "@posthog/shared"; import { useAddDirectoryDialogStore } from "@posthog/ui/features/folder-picker/addDirectoryDialogStore"; import { toast } from "@posthog/ui/primitives/toast"; import { useQuery } from "@tanstack/react-query"; +import { SquareSlash } from "lucide-react"; import { useRef, useState } from "react"; import { getGhStatus, selectAttachments } from "../hostApi"; import { @@ -32,6 +33,12 @@ interface AttachmentMenuProps { onAttachFiles?: (files: File[]) => void; onInsertChip: (chip: MentionChip) => void; onRemoveChip?: (chipId: string) => void; + /** + * Writes a slash at the start of the composer, opening the command list the + * same way typing one does. Omitted where the menu has no editor to write + * into, which hides the item. + */ + onInsertSlashCommand?: () => void; iconSize?: number; attachTooltip?: string; } @@ -56,6 +63,7 @@ export function AttachmentMenu({ onAttachFiles, onInsertChip, onRemoveChip, + onInsertSlashCommand, iconSize = 14, attachTooltip = "Attach", }: AttachmentMenuProps) { @@ -162,6 +170,13 @@ export function AttachmentMenu({ setIssuePickerOpen(true); }; + // Close first: the command list opens against the composer, and leaving this + // menu up would stack one popup over the other. + const handleInsertSlashCommand = () => { + setMenuOpen(false); + onInsertSlashCommand?.(); + }; + const handleIssueSelect = (chip: MentionChip) => { onInsertChip(chip); setIssuePickerOpen(false); @@ -201,17 +216,17 @@ export function AttachmentMenu({ {isWindows ? ( <> - + Add file - + Add folder ) : ( - + Add file or folder )} @@ -220,9 +235,18 @@ export function AttachmentMenu({ onClick={handleOpenIssuePicker} title={issueDisabledReason ?? undefined} > - + Add issue or pull request + {onInsertSlashCommand && ( + + {/* Lucide's default stroke is heavier than Phosphor's regular + weight at the same size: 2/24 of the viewBox against 16/256. + 1.5 lands on the same rendered thickness as the icons above. */} + + Slash commands + + )} ) : undefined } - attachmentsPrefix={ + submitAdornment={ channelContext ? ( {}} /> ) : undefined diff --git a/products/desktop/packages/ui/src/features/message-editor/components/PromptInput.tsx b/products/desktop/packages/ui/src/features/message-editor/components/PromptInput.tsx index f8511405c50f..a2c90598aa48 100644 --- a/products/desktop/packages/ui/src/features/message-editor/components/PromptInput.tsx +++ b/products/desktop/packages/ui/src/features/message-editor/components/PromptInput.tsx @@ -16,7 +16,13 @@ import { hasOpenOverlay } from "@posthog/ui/utils/overlay"; import { Flex, Text, Tooltip } from "@radix-ui/themes"; import { EditorContent } from "@tiptap/react"; import clsx from "clsx"; -import { forwardRef, useCallback, useEffect, useImperativeHandle } from "react"; +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useState, +} from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useSkills } from "../../skills/useSkills"; import { skillToEditorCommand } from "../commands"; @@ -72,11 +78,14 @@ export interface PromptInputProps { messagingModeToggle?: React.ReactNode; historyButton?: React.ReactNode; /** - * Rendered in the attachments row at the top of the composer, ahead of the - * attachments themselves — for context the prompt carries that the user did - * not attach by hand (e.g. a channel's CONTEXT.md). + * Pinned inside the composer box beside the send button — for context the + * prompt always carries that the user did not attach by hand (e.g. a + * channel's CONTEXT.md). It sits apart from the attachments row on purpose: + * hand-picked files come and go, this rides along with every send. The + * editor reserves its measured width, so keep it a fixed size rather than + * one that changes on hover. */ - attachmentsPrefix?: React.ReactNode; + submitAdornment?: React.ReactNode; /** * Pushed to the far end of the composer's toolbar row — for read-only status * about the session the prompt goes to (e.g. context usage), as opposed to @@ -149,7 +158,7 @@ export const PromptInput = forwardRef( reasoningSelector, messagingModeToggle, historyButton, - attachmentsPrefix, + submitAdornment, toolbarEndSlot, headerAddon, hideDefaultToolbar = false, @@ -180,6 +189,24 @@ export const PromptInput = forwardRef( const clearFocusRequest = useDraftStore((s) => s.actions.clearFocusRequest); const slotMachineMode = useSettingsStore((s) => s.slotMachineMode); const { data: skills } = useSkills(); + // Seeded at the send button's own width so the first paint already clears + // it, rather than laying the text out full-width and reflowing it. + const [submitClusterWidth, setSubmitClusterWidth] = useState(40); + // The text's right inset has to clear whatever sits over its bottom-right + // corner. That used to be the send button alone (a fixed 40px), but an + // adornment beside it makes the width depend on its content, so measure. + // A callback ref rather than an effect: the cluster mounts and unmounts + // with the button, and this re-observes the new node each time. + const submitClusterRef = useCallback((el: HTMLSpanElement | null) => { + if (!el) return; + const observer = new ResizeObserver(([entry]) => { + // The cluster is inset by 4px (right-1); the same again keeps the text + // from running up against it. + setSubmitClusterWidth(entry.contentRect.width + 8); + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); const { editor, @@ -195,6 +222,7 @@ export const PromptInput = forwardRef( setContent, insertEditorContent, insertChip, + insertSlashCommand, removeChipById, replaceChipAttrs, attachments, @@ -437,6 +465,7 @@ export const PromptInput = forwardRef( onAttachFiles={onAttachFiles} onInsertChip={insertChip} onRemoveChip={removeChipById} + onInsertSlashCommand={insertSlashCommand} /> {onModeChange && ( ( canvas={canvas} /> )} - {modelSelector && {modelSelector}} - {reasoningSelector && {reasoningSelector}} + {/* Direct flex children, not wrapped in a span: an inline wrapper + builds a line box whose leading pushes the trigger a pixel below + the toolbar's other buttons. */} + {modelSelector} + {reasoningSelector} {isBashMode && ( ! bash @@ -479,12 +511,11 @@ export const PromptInput = forwardRef( {headerAddon && ( {headerAddon} )} - {(attachmentsPrefix || attachments.length > 0) && ( + {attachments.length > 0 && ( {/* One provider for the row: moving between squares reuses the open delay instead of re-waiting it per attachment. */} - {attachmentsPrefix} ( column beside it. Laid out beside the text, it would push the scroll container inwards and strand the scrollbar mid-box; over it, the container runs to the edge and the bar hugs it. The text - reserves the button's width so a long line never runs underneath. */} + reserves the cluster's width so a long line never runs underneath + — measured rather than fixed, because an adornment beside the + button makes that width depend on what's in it. */}
( >
- {submitButton && ( - {submitButton} + {(submitButton || submitAdornment) && ( + + {submitAdornment} + {submitButton} + )}
diff --git a/products/desktop/packages/ui/src/features/message-editor/tiptap/MentionChipView.tsx b/products/desktop/packages/ui/src/features/message-editor/tiptap/MentionChipView.tsx index 5b0e54226455..ccc3d6b508ee 100644 --- a/products/desktop/packages/ui/src/features/message-editor/tiptap/MentionChipView.tsx +++ b/products/desktop/packages/ui/src/features/message-editor/tiptap/MentionChipView.tsx @@ -10,7 +10,7 @@ import { WarningIcon, XIcon, } from "@phosphor-icons/react"; -import { Chip } from "@posthog/quill"; +import { Chip, cn } from "@posthog/quill"; import { Tooltip } from "@posthog/ui/primitives/Tooltip"; import { type NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { usePasteUndoStore } from "../pasteUndoStore"; @@ -88,6 +88,28 @@ function DefaultChip({ const isGithubRef = type === "github_issue" || type === "github_pr"; const canOpenUrl = isGithubRef && /^https:\/\//.test(id); + // A skill reads as part of the sentence being written, not as an object + // attached to it, so it stays plain text rather than taking a chip's border + // and remove button. Selecting it — arrowing onto it, or the backspace that + // is about to delete it — turns the run destructive, which is the only + // warning left once there is no × to aim at. + if (isCommand) { + return ( + + {prefix} + {label} + + ); + } + const chipContent = ( )} - + {item.label} {item.description && ( @@ -113,30 +117,31 @@ export const SuggestionList = forwardRef<
!hasMouseMoved && setHasMouseMoved(true)} > - {items.map((item, index) => { - const isSelected = index === selectedIndex; - return ( - - ); - })} + {items.map((item, index) => ( + { + itemRefs.current[index] = el; + }} + size="xs" + // `option` inside the listbox above, not the `menuitem` this + // primitive defaults to. Selection is the plugin's, driven by the + // keys it forwards, so it rides on aria-selected rather than the + // focus these rows never take (the caret stays in the editor). + role="option" + aria-selected={index === selectedIndex} + onClick={() => command(item)} + onMouseEnter={() => hasMouseMoved && setSelectedIndex(index)} + className="w-full text-left aria-selected:bg-fill-selected" + > + {renderItem ? renderItem(item) : } + + ))}
-
+
navigate diff --git a/products/desktop/packages/ui/src/features/message-editor/tiptap/useTiptapEditor.ts b/products/desktop/packages/ui/src/features/message-editor/tiptap/useTiptapEditor.ts index a904f7912592..55c23b235c88 100644 --- a/products/desktop/packages/ui/src/features/message-editor/tiptap/useTiptapEditor.ts +++ b/products/desktop/packages/ui/src/features/message-editor/tiptap/useTiptapEditor.ts @@ -904,6 +904,16 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { [editor, draft, attachments], ); + // Position 1 is the start of the first paragraph. Writing the slash there + // and dropping the caret after it leaves the editor in exactly the state + // typing "/" would, which is what the suggestion plugin watches for, so the + // command list opens on its own rather than needing to be opened here. + const insertSlashCommand = useCallback(() => { + if (!editor) return; + editor.chain().insertContentAt(1, "/").focus(2).run(); + draft.saveDraft(editor, attachments); + }, [editor, draft, attachments]); + const removeChipById = useCallback( (chipId: string) => { if (!editor) return; @@ -964,6 +974,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { setContent, insertEditorContent, insertChip, + insertSlashCommand, removeChipById, replaceChipAttrs, attachments, diff --git a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx index 3c37b3ae2e0c..83c988e81c1b 100644 --- a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx +++ b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx @@ -1,10 +1,18 @@ -import { Attachment } from "@posthog/ui/features/message-editor/components/Attachment"; +import { + Chip, + ChipClose, + cn, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@posthog/quill"; /** - * The channel CONTEXT.md riding along with the prompt, shown in the composer's - * attachments row. Deliberately the same component the user's own attachments - * use, because that is what it is from the writer's point of view: something - * extra travelling with the message, which they can inspect or drop. + * The channel CONTEXT.md riding along with the prompt, pinned inside the + * composer beside the send button rather than sitting in the attachments row. + * It isn't an attachment the writer chose: it comes with the space and travels + * with every prompt sent from it, so it keeps its own spot and shows its name + * instead of collapsing to an extension square like a real attachment does. */ export function ChannelContextChip({ channelName, @@ -15,12 +23,34 @@ export function ChannelContextChip({ onView?: () => void; onRemove: () => void; }) { + const chip = ( + + CONTEXT.md + {/* Always rendered, not revealed on hover: the chip sets the editor's + right padding, so a width that changes under the cursor would reflow + the text being typed. */} + { + // The chip itself opens the file; removing it must not also do that. + event.stopPropagation(); + onRemove(); + }} + /> + + ); + + if (!onView) return chip; + return ( - + + + Click to view CONTEXT.md + ); } diff --git a/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx b/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx index b631b1ec46ab..dbd920e45895 100644 --- a/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/products/desktop/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -24,7 +24,14 @@ import { track } from "@posthog/ui/shell/analytics"; import { Box, 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 { + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { useConnectivity } from "../../../hooks/useConnectivity"; import { DotPatternBackground } from "../../../primitives/DotPatternBackground"; import { toast } from "../../../primitives/toast"; @@ -152,6 +159,12 @@ interface TaskInputProps { * the chip is non-interactive (only dismissable). */ onContextChipClick?: () => void; + /** + * A space picker chip rendered first in the selector row above the composer + * (beside the workspace-mode chip). Channels new-task screen only — /code + * has no spaces to pick. + */ + spaceSelector?: ReactNode; } export function TaskInput({ @@ -171,6 +184,7 @@ export function TaskInput({ suggestions, onSuggestionSelect, onContextChipClick, + spaceSelector, }: TaskInputProps = {}) { const cloudRegion = useAuthStateValue((s) => s.cloudRegion); const trpc = useHostTRPC(); @@ -1093,7 +1107,6 @@ export function TaskInput({ useDraftStore.getState().actions.clearCommands(promptSessionId); }; }, [promptSessionId, skills]); - const hasHistory = useTaskInputHistoryStore((s) => s.entries.length > 0); const getPromptHistory = useCallback( () => useTaskInputHistoryStore.getState().entries.map((e) => e.text), [], @@ -1106,13 +1119,6 @@ export function TaskInput({ () => !(editorRef.current?.isEmpty() ?? true), [], ); - const hints = [ - "@ to add files", - "/ for skills", - hasHistory ? "\u2191\u2193 for history" : "", - ] - .filter(Boolean) - .join(", "); useAutoFocusOnTyping(editorRef, isCreatingTask); @@ -1200,13 +1206,14 @@ export function TaskInput({ 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" + className="absolute left-1/2 z-1 flex w-[calc(100%-2rem)] max-w-[600px] flex-col gap-2" > + {spaceSelector} {piHarnessEnabled && ( {triggerIcon} {triggerLabel} - } /> From 4cc0b503cb8dee486b29a191c92b5514334c3489 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 4 Aug 2026 16:15:08 +0100 Subject: [PATCH 2/5] style(desktop): nudge the context chip onto the send button's baseline Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01J4jadVYefktsDCvLvGXDKP --- .../features/task-detail/components/ChannelContextChip.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx index 83c988e81c1b..4096f96a97b1 100644 --- a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx +++ b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx @@ -27,7 +27,10 @@ export function ChannelContextChip({ CONTEXT.md {/* Always rendered, not revealed on hover: the chip sets the editor's From 16465030fd6338dd6d8f072a6ad09cbbf6443bbd Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Tue, 4 Aug 2026 16:20:58 +0100 Subject: [PATCH 3/5] fix(desktop): make the context chip activate on Enter Chip renders a div so its close button can nest legally, but the button behaviour underneath assumes a native button unless told otherwise, so it never bound Enter or Space. The chip took focus and then did nothing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01J4jadVYefktsDCvLvGXDKP --- .../features/task-detail/components/ChannelContextChip.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx index 4096f96a97b1..9a80b3801586 100644 --- a/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx +++ b/products/desktop/packages/ui/src/features/task-detail/components/ChannelContextChip.tsx @@ -27,6 +27,11 @@ export function ChannelContextChip({ so ChipClose can nest legally, but the button + // behaviour underneath assumes a real