From 611df4ed74699241ee8ff26279e23c30713a0024 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 1 Jul 2026 23:00:30 +0000 Subject: [PATCH] PAP-12316 add issue chat thread presets --- ui/src/components/IssueChatThread.tsx | 187 ++++++++++-------- ui/src/components/SelectedAgentChat.test.tsx | 10 +- ui/src/components/SelectedAgentChat.tsx | 5 +- ui/src/components/chat-thread-presets.test.ts | 69 +++++++ ui/src/components/chat-thread-presets.ts | 116 +++++++++++ 5 files changed, 298 insertions(+), 89 deletions(-) create mode 100644 ui/src/components/chat-thread-presets.test.ts create mode 100644 ui/src/components/chat-thread-presets.ts diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index d778af0599a..0bf2e030592 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -174,6 +174,10 @@ import { IssueRecoveryActionCard, type RecoveryResolveOutcome } from "./IssueRec import { SourceTrustBadge } from "./SourceTrustBadge"; import { AnimatedPaperclipIcon } from "./AnimatedPaperclipIcon"; import { BoardChatBackgroundWorkCard } from "./BoardChatBackgroundWorkCard"; +import { + resolveIssueChatThreadPreset, + type IssueChatThreadPresetName, +} from "./chat-thread-presets"; interface IssueChatMessageContext { feedbackDataSharingPreference: FeedbackDataSharingPreference; @@ -356,6 +360,7 @@ interface IssueChatComposerProps { } interface IssueChatThreadProps { + preset?: IssueChatThreadPresetName; comments: IssueChatComment[]; interactions?: IssueThreadInteraction[]; feedbackVotes?: FeedbackVote[]; @@ -415,6 +420,7 @@ interface IssueChatThreadProps { onWorkModeChange?: (workMode: IssueWorkMode) => Promise | void; showComposer?: boolean; showJumpToLatest?: boolean; + showBackgroundWorkChildren?: boolean; autoScrollToLatestOnInitialLoad?: boolean; emptyMessage?: string; emptyState?: ReactNode; @@ -4118,82 +4124,94 @@ const IssueChatComposer = forwardRef(null); const virtualizedThreadRef = useRef(null); @@ -4759,11 +4777,8 @@ export function IssueChatThread({ ], ); - const resolvedShowJumpToLatest = showJumpToLatest ?? variant === "full"; - const resolvedEmptyMessage = emptyMessage - ?? (variant === "embedded" - ? "No run output yet." - : "This task conversation is empty. Start with a message below."); + const resolvedShowJumpToLatest = showJumpToLatest; + const resolvedEmptyMessage = emptyMessage; const previousErrorBoundaryMessagesRef = useRef(null); const errorBoundaryResetVersionRef = useRef(0); if (previousErrorBoundaryMessagesRef.current !== messages) { @@ -4838,10 +4853,12 @@ export function IssueChatThread({ )} {showComposer ? (
- + {showBackgroundWorkChildren ? ( + + ) : null} {!suppressIssueStatusNotices ? ( <> ({ IssueChatThread: (props: { + preset?: string; emptyMessage?: string; emptyState?: ReactNode; showJumpToLatest?: boolean; @@ -26,12 +27,15 @@ vi.mock("./IssueChatThread", () => ({ onAttachImage?: (file: File) => Promise; }) => (
+ {props.preset} {props.emptyState ? "" : props.emptyMessage}
{props.emptyState}
- {String(props.showJumpToLatest)} + + {String(props.showJumpToLatest ?? (props.preset === "assistant" ? false : true))} + {props.backgroundWorkChildren?.length ?? 0} - {props.suppressIssueStatusNotices ? "suppressed" : "visible"} + {(props.suppressIssueStatusNotices ?? props.preset === "assistant") ? "suppressed" : "visible"} {props.composerHint} {String(Boolean(props.imageUploadHandler))} @@ -263,6 +267,8 @@ describe("SelectedAgentChatView", () => { "Send Sarah a message to start the conversation.", ); expect(container.querySelector('[data-testid="jump-to-latest"]')?.textContent).toBe("false"); + expect(container.querySelector('[data-testid="thread-preset"]')?.textContent).toBe("assistant"); + expect(container.querySelector('[data-testid="status-notices"]')?.textContent).toBe("suppressed"); }); it("forwards attachment handlers to the shared issue chat composer", () => { diff --git a/ui/src/components/SelectedAgentChat.tsx b/ui/src/components/SelectedAgentChat.tsx index ccfb0cda0be..eed7ca54366 100644 --- a/ui/src/components/SelectedAgentChat.tsx +++ b/ui/src/components/SelectedAgentChat.tsx @@ -174,7 +174,7 @@ export function SelectedAgentChatView({ projectId, currentUserId, backgroundWorkChildren = [], - suppressIssueStatusNotices = false, + suppressIssueStatusNotices, conferenceRoomMode = false, companyName = null, composerHint = null, @@ -216,6 +216,7 @@ export function SelectedAgentChatView({ ); const targetName = targetAgent?.name ?? "Assistant"; + const threadPreset = conferenceRoomMode ? "assistant" : "task"; const targetRole = targetAgent ? roleLabels[targetAgent.role] ?? targetAgent.role : null; const canSwitch = showAgentSwitcher && invokableAgents.length > 1 && !!onTargetAgentChange; const starterPrompts = useMemo( @@ -333,6 +334,7 @@ export function SelectedAgentChatView({ className="min-w-0 px-4 pb-4 pt-3" >
)} diff --git a/ui/src/components/chat-thread-presets.test.ts b/ui/src/components/chat-thread-presets.test.ts new file mode 100644 index 00000000000..7a94438127f --- /dev/null +++ b/ui/src/components/chat-thread-presets.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { resolveIssueChatThreadPreset } from "./chat-thread-presets"; + +describe("resolveIssueChatThreadPreset", () => { + it("treats an omitted preset exactly like the task preset", () => { + expect(resolveIssueChatThreadPreset()).toEqual(resolveIssueChatThreadPreset("task")); + }); + + it("defaults to the current task thread behavior", () => { + expect(resolveIssueChatThreadPreset()).toMatchObject({ + preset: "task", + density: "comfortable", + workingIndicator: "expanded", + variant: "full", + showComposer: true, + showJumpToLatest: true, + suppressIssueStatusNotices: false, + enableReassign: false, + showBackgroundWorkChildren: true, + showRunFinalizationActions: true, + composerSubmitKey: "mod-enter", + composerSingleLine: false, + }); + }); + + it("keeps task defaults variant-aware", () => { + expect(resolveIssueChatThreadPreset("task", { variant: "embedded" })).toMatchObject({ + variant: "embedded", + showJumpToLatest: false, + emptyMessage: "No run output yet.", + }); + }); + + it("maps the assistant preset to the lightweight chat defaults", () => { + expect(resolveIssueChatThreadPreset("assistant")).toMatchObject({ + preset: "assistant", + density: "compact", + workingIndicator: "collapsed", + variant: "full", + showComposer: true, + showJumpToLatest: false, + suppressIssueStatusNotices: true, + enableReassign: false, + showBackgroundWorkChildren: false, + showRunFinalizationActions: false, + composerSubmitKey: "enter", + composerSingleLine: true, + composerMentions: "optional", + }); + }); + + it("lets explicit props override preset defaults", () => { + expect(resolveIssueChatThreadPreset("assistant", { + showJumpToLatest: true, + suppressIssueStatusNotices: false, + showBackgroundWorkChildren: true, + showComposer: false, + enableReassign: true, + emptyMessage: "Custom empty state", + })).toMatchObject({ + showJumpToLatest: true, + suppressIssueStatusNotices: false, + showBackgroundWorkChildren: true, + showComposer: false, + enableReassign: true, + emptyMessage: "Custom empty state", + }); + }); +}); diff --git a/ui/src/components/chat-thread-presets.ts b/ui/src/components/chat-thread-presets.ts new file mode 100644 index 00000000000..82eba454a3f --- /dev/null +++ b/ui/src/components/chat-thread-presets.ts @@ -0,0 +1,116 @@ +export type IssueChatThreadPresetName = "task" | "assistant"; + +export type IssueChatThreadVariant = "full" | "embedded"; + +export type IssueChatThreadDensity = "comfortable" | "compact"; + +export type IssueChatThreadWorkingIndicator = "expanded" | "collapsed"; + +export type IssueChatThreadComposerSubmitKey = "mod-enter" | "enter"; + +export interface IssueChatThreadPresetConfig { + preset: IssueChatThreadPresetName; + density: IssueChatThreadDensity; + workingIndicator: IssueChatThreadWorkingIndicator; + variant: IssueChatThreadVariant; + showComposer: boolean; + showJumpToLatest: boolean; + autoScrollToLatestOnInitialLoad: boolean; + emptyMessage: string; + suppressIssueStatusNotices: boolean; + enableReassign: boolean; + showBackgroundWorkChildren: boolean; + showRunFinalizationActions: boolean; + composerSubmitKey: IssueChatThreadComposerSubmitKey; + composerSingleLine: boolean; + composerMentions: "enabled" | "optional"; +} + +export interface IssueChatThreadPresetOverrides { + variant?: IssueChatThreadVariant; + showComposer?: boolean; + showJumpToLatest?: boolean; + autoScrollToLatestOnInitialLoad?: boolean; + emptyMessage?: string; + suppressIssueStatusNotices?: boolean; + enableReassign?: boolean; + showBackgroundWorkChildren?: boolean; +} + +type IssueChatThreadPresetDefinition = Omit< + IssueChatThreadPresetConfig, + "preset" | "variant" | "showJumpToLatest" | "emptyMessage" +> & { + variant: IssueChatThreadVariant; + showJumpToLatest: (variant: IssueChatThreadVariant) => boolean; + emptyMessage: (variant: IssueChatThreadVariant) => string; +}; + +const taskEmptyMessage = (variant: IssueChatThreadVariant) => + variant === "embedded" + ? "No run output yet." + : "This task conversation is empty. Start with a message below."; + +const ISSUE_CHAT_THREAD_PRESETS: Record = { + task: { + density: "comfortable", + workingIndicator: "expanded", + variant: "full", + showComposer: true, + showJumpToLatest: (variant) => variant === "full", + autoScrollToLatestOnInitialLoad: true, + emptyMessage: taskEmptyMessage, + suppressIssueStatusNotices: false, + enableReassign: false, + showBackgroundWorkChildren: true, + showRunFinalizationActions: true, + composerSubmitKey: "mod-enter", + composerSingleLine: false, + composerMentions: "enabled", + }, + assistant: { + density: "compact", + workingIndicator: "collapsed", + variant: "full", + showComposer: true, + showJumpToLatest: () => false, + autoScrollToLatestOnInitialLoad: true, + emptyMessage: () => "Send a message to start the conversation.", + suppressIssueStatusNotices: true, + enableReassign: false, + showBackgroundWorkChildren: false, + showRunFinalizationActions: false, + composerSubmitKey: "enter", + composerSingleLine: true, + composerMentions: "optional", + }, +}; + +export function resolveIssueChatThreadPreset( + preset: IssueChatThreadPresetName = "task", + overrides: IssueChatThreadPresetOverrides = {}, +): IssueChatThreadPresetConfig { + const definition = ISSUE_CHAT_THREAD_PRESETS[preset]; + const variant = overrides.variant ?? definition.variant; + + return { + preset, + density: definition.density, + workingIndicator: definition.workingIndicator, + variant, + showComposer: overrides.showComposer ?? definition.showComposer, + showJumpToLatest: overrides.showJumpToLatest ?? definition.showJumpToLatest(variant), + autoScrollToLatestOnInitialLoad: + overrides.autoScrollToLatestOnInitialLoad ?? definition.autoScrollToLatestOnInitialLoad, + emptyMessage: overrides.emptyMessage ?? definition.emptyMessage(variant), + suppressIssueStatusNotices: + overrides.suppressIssueStatusNotices ?? definition.suppressIssueStatusNotices, + enableReassign: overrides.enableReassign ?? definition.enableReassign, + showBackgroundWorkChildren: + overrides.showBackgroundWorkChildren ?? definition.showBackgroundWorkChildren, + showRunFinalizationActions: definition.showRunFinalizationActions, + composerSubmitKey: definition.composerSubmitKey, + composerSingleLine: definition.composerSingleLine, + composerMentions: definition.composerMentions, + }; +}