Skip to content
Open
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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default defineConfig({
"**/settings-section-layout.spec.ts",
"**/experimental-features.spec.ts",
"**/interactions.spec.ts",
"**/interaction-authoring.spec.ts",
"**/agent-provider-dropdowns.spec.ts",
"**/agent-lifecycle-feedback.spec.ts",
"**/agent-access-warning.spec.ts",
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMediaUpload } from "@/features/messages/lib/useMediaUpload";
import { ComposerDockBackdrop } from "@/features/messages/ui/ComposerDockBackdrop";
import { ComposerUploadProgressOverlay } from "@/features/messages/ui/ComposerUploadProgressOverlay";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
import { AskInteractionButton } from "@/features/interactions/AskInteractionButton";
import { ComposerTimeoutBanner } from "@/features/moderation/ui/ComposerTimeoutBanner";
import { useTimeoutState } from "@/features/moderation/lib/timeoutStore";
import { isModerationDm } from "@/features/moderation/lib/moderationDm";
Expand Down Expand Up @@ -267,6 +268,16 @@ export const ChannelPane = React.memo(function ChannelPane({
timeoutState.active ||
isModerationDmChannel ||
isSending;
// Memoized so the memoized composer does not re-render for a fresh element.
const askInteractionAction = React.useMemo(
() => (
<AskInteractionButton
channelId={activeChannel?.id ?? null}
disabled={isComposerDisabled}
/>
),
[activeChannel?.id, isComposerDisabled],
);
const knownAgentPubkeys = React.useMemo(() => {
const pubkeys = new Set<string>();
for (const pubkey of agentPubkeys ?? []) {
Expand Down Expand Up @@ -778,6 +789,7 @@ export const ChannelPane = React.memo(function ChannelPane({
onSend={handleSendMessage}
{...{ profiles, recentMentionPubkeys: recentMentions }}
showBackgroundUploadProgress={false}
toolbarExtraActions={askInteractionAction}
placeholder={
timeoutState.active
? "You're timed out by community moderators."
Expand Down
58 changes: 58 additions & 0 deletions desktop/src/features/interactions/AskInteractionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ListChecks } from "lucide-react";
import * as React from "react";

import { useFeatureEnabled } from "@/shared/features";
import { Button } from "@/shared/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { AskInteractionDialog } from "./AskInteractionDialog";

/**
* Composer action that opens the prompt authoring dialog. Renders nothing
* unless the "Interaction cards" experimental feature is enabled, so default
* builds keep their toolbar unchanged.
*
* The dialog is pinned to the channel it was opened for: it publishes only
* to that channel, closes if the active channel changes underneath it, and
* its draft is keyed per channel so text written for one channel is never
* carried into another.
*/
export const AskInteractionButton = React.memo(function AskInteractionButton({
channelId,
disabled = false,
}: {
channelId: string | null;
disabled?: boolean;
}) {
const enabled = useFeatureEnabled("interactions");
const [openFor, setOpenFor] = React.useState<string | null>(null);
if (!enabled) return null;
const open = openFor !== null && openFor === channelId;
return (
<>
<Tooltip disableHoverableContent>
<TooltipTrigger asChild>
<Button
aria-label="Ask for a decision"
data-testid="ask-interaction"
disabled={disabled || !channelId}
onClick={() => setOpenFor(channelId)}
size="icon"
type="button"
variant="ghost"
>
<ListChecks />
</Button>
</TooltipTrigger>
<TooltipContent>Ask for a decision</TooltipContent>
</Tooltip>
{channelId ? (
<AskInteractionDialog
key={channelId}
channelId={channelId}
open={open}
onOpenChange={(next) => setOpenFor(next ? channelId : null)}
/>
) : null}
</>
);
});
Loading