mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - A core part of that is the operator experience around reading issue state, agent chat, and sub-task structure > - The current branch had a long run of issue-detail and issue-list UX fixes that all improve how humans follow and steer active work > - Those changes mostly live in the UI/chat surface and should be reviewed together instead of mixed with workspace/runtime work > - This pull request packages the issue-detail, chat, markdown, and sub-issue list improvements into one standalone change > - The benefit is a cleaner, less jumpy, more reliable issue workflow on desktop and mobile without coupling it to unrelated server/runtime refactors ## What Changed - Stabilized issue chat runtime wiring, optimistic comment handling, queued-comment cancellation, and composer anchoring during live updates - Fixed several issue-detail rendering and navigation regressions including placeholder bleed, local polling scope, mobile inbox-to-issue transitions, and visible refresh resets - Improved markdown and rich-content handling with advisory image normalization, editor fallback behavior, touch mention recovery, and `issue:` quicklook links - Refined sub-issue behavior with parent-derived defaults, current-user inheritance fixes, empty-state cleanup, and a reusable issue-list presentation for sub-issues - Added targeted UI tests for the new issue-detail, chat scroll/message, placeholder-data, markdown, and issue-list behaviors ## Verification - `pnpm vitest run ui/src/components/IssueChatThread.test.tsx ui/src/components/MarkdownEditor.test.tsx ui/src/components/IssuesList.test.tsx ui/src/context/LiveUpdatesProvider.test.tsx ui/src/lib/issue-chat-messages.test.ts ui/src/lib/issue-chat-scroll.test.ts ui/src/lib/issue-detail-subissues.test.ts ui/src/lib/query-placeholder-data.test.tsx ui/src/hooks/usePaperclipIssueRuntime.test.tsx` ## Risks - Medium: this branch touches the highest-traffic issue-detail UI paths, so regressions would show up as chat/thread or sub-issue UX glitches - The changes are UI-heavy and would benefit from reviewer screenshots or a quick manual browser pass before merge ## Model Used - OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact deployed model ID is not exposed in this environment), reasoning enabled, tool use and local code execution enabled ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
5d1ed71779
commit
6e6f538630
41 changed files with 4141 additions and 590 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
|
|
@ -36,8 +37,10 @@ import { usePaperclipIssueRuntime, type PaperclipIssueRuntimeReassignment } from
|
|||
import {
|
||||
buildIssueChatMessages,
|
||||
formatDurationWords,
|
||||
stabilizeThreadMessages,
|
||||
type IssueChatComment,
|
||||
type IssueChatLinkedRun,
|
||||
type StableThreadMessageCacheEntry,
|
||||
type IssueChatTranscriptEntry,
|
||||
type SegmentTiming,
|
||||
} from "../lib/issue-chat-messages";
|
||||
|
|
@ -65,6 +68,11 @@ import { Identity } from "./Identity";
|
|||
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { restoreSubmittedCommentDraft } from "../lib/comment-submit-draft";
|
||||
import {
|
||||
captureComposerViewportSnapshot,
|
||||
restoreComposerViewportSnapshot,
|
||||
shouldPreserveComposerViewport,
|
||||
} from "../lib/issue-chat-scroll";
|
||||
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import {
|
||||
|
|
@ -80,7 +88,7 @@ import { cn, formatDateTime, formatShortDate } from "../lib/utils";
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AlertTriangle, ArrowRight, Brain, Check, ChevronDown, Copy, Hammer, Loader2, MoreHorizontal, Paperclip, Search, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
import { AlertTriangle, ArrowRight, Brain, Check, ChevronDown, Copy, Hammer, Loader2, MoreHorizontal, Paperclip, Search, Square, ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
|
||||
interface IssueChatMessageContext {
|
||||
feedbackVoteByTargetId: Map<string, FeedbackVoteValue>;
|
||||
|
|
@ -88,12 +96,16 @@ interface IssueChatMessageContext {
|
|||
feedbackTermsUrl: string | null;
|
||||
agentMap?: Map<string, Agent>;
|
||||
currentUserId?: string | null;
|
||||
activeRunIds: ReadonlySet<string>;
|
||||
onVote?: (
|
||||
commentId: string,
|
||||
vote: FeedbackVoteValue,
|
||||
options?: { allowSharing?: boolean; reason?: string },
|
||||
) => Promise<void>;
|
||||
onStopRun?: (runId: string) => Promise<void>;
|
||||
stoppingRunId?: string | null;
|
||||
onInterruptQueued?: (runId: string) => Promise<void>;
|
||||
onCancelQueued?: (commentId: string) => void;
|
||||
interruptingQueuedRunId?: string | null;
|
||||
onImageClick?: (src: string) => void;
|
||||
}
|
||||
|
|
@ -102,6 +114,7 @@ const IssueChatCtx = createContext<IssueChatMessageContext>({
|
|||
feedbackVoteByTargetId: new Map(),
|
||||
feedbackDataSharingPreference: "prompt",
|
||||
feedbackTermsUrl: null,
|
||||
activeRunIds: new Set<string>(),
|
||||
});
|
||||
|
||||
export function resolveAssistantMessageFoldedState(args: {
|
||||
|
|
@ -125,6 +138,17 @@ export function resolveAssistantMessageFoldedState(args: {
|
|||
return currentFolded;
|
||||
}
|
||||
|
||||
export function canStopIssueChatRun(args: {
|
||||
runId: string | null;
|
||||
runStatus: string | null;
|
||||
activeRunIds: ReadonlySet<string>;
|
||||
}) {
|
||||
const { runId, runStatus, activeRunIds } = args;
|
||||
if (!runId) return false;
|
||||
if (activeRunIds.has(runId)) return true;
|
||||
return runStatus === "queued" || runStatus === "running";
|
||||
}
|
||||
|
||||
function findCoTSegmentIndex(
|
||||
messageParts: ReadonlyArray<{ type: string }>,
|
||||
cotParts: ReadonlyArray<{ type: string }>,
|
||||
|
|
@ -162,6 +186,7 @@ interface CommentReassignment {
|
|||
|
||||
export interface IssueChatComposerHandle {
|
||||
focus: () => void;
|
||||
restoreDraft: (submittedBody: string) => void;
|
||||
}
|
||||
|
||||
interface IssueChatComposerProps {
|
||||
|
|
@ -199,6 +224,7 @@ interface IssueChatThreadProps {
|
|||
) => Promise<void>;
|
||||
onAdd: (body: string, reopen?: boolean, reassignment?: CommentReassignment) => Promise<void>;
|
||||
onCancelRun?: () => Promise<void>;
|
||||
onStopRun?: (runId: string) => Promise<void>;
|
||||
imageUploadHandler?: (file: File) => Promise<string>;
|
||||
onAttachImage?: (file: File) => Promise<void>;
|
||||
draftKey?: string;
|
||||
|
|
@ -217,7 +243,9 @@ interface IssueChatThreadProps {
|
|||
hasOutputForRun?: (runId: string) => boolean;
|
||||
includeSucceededRunsWithoutOutput?: boolean;
|
||||
onInterruptQueued?: (runId: string) => Promise<void>;
|
||||
onCancelQueued?: (commentId: string) => void;
|
||||
interruptingQueuedRunId?: string | null;
|
||||
stoppingRunId?: string | null;
|
||||
onImageClick?: (src: string) => void;
|
||||
composerRef?: Ref<IssueChatComposerHandle>;
|
||||
}
|
||||
|
|
@ -412,6 +440,11 @@ function parseReassignment(target: string): PaperclipIssueRuntimeReassignment |
|
|||
return null;
|
||||
}
|
||||
|
||||
function shouldImplicitlyReopenComment(issueStatus: string | undefined, assigneeValue: string) {
|
||||
const isClosed = issueStatus === "done" || issueStatus === "cancelled";
|
||||
return isClosed && assigneeValue.startsWith("agent:");
|
||||
}
|
||||
|
||||
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function commentDateLabel(date: Date | string | undefined): string {
|
||||
|
|
@ -873,10 +906,11 @@ function IssueChatToolPart({
|
|||
}
|
||||
|
||||
function IssueChatUserMessage() {
|
||||
const { onInterruptQueued, interruptingQueuedRunId } = useContext(IssueChatCtx);
|
||||
const { onInterruptQueued, onCancelQueued, interruptingQueuedRunId } = useContext(IssueChatCtx);
|
||||
const message = useMessage();
|
||||
const custom = message.metadata.custom as Record<string, unknown>;
|
||||
const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined;
|
||||
const commentId = typeof custom.commentId === "string" ? custom.commentId : message.id;
|
||||
const queued = custom.queueState === "queued" || custom.clientStatus === "queued";
|
||||
const pending = custom.clientStatus === "pending";
|
||||
const queueTargetRunId = typeof custom.queueTargetRunId === "string" ? custom.queueTargetRunId : null;
|
||||
|
|
@ -911,6 +945,16 @@ function IssueChatUserMessage() {
|
|||
{interruptingQueuedRunId === queueTargetRunId ? "Interrupting..." : "Interrupt"}
|
||||
</Button>
|
||||
) : null}
|
||||
{onCancelQueued ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 border-amber-300 px-2 text-[11px] text-amber-900 hover:bg-amber-100/80 hover:text-amber-950 dark:border-amber-500/40 dark:text-amber-100 dark:hover:bg-amber-500/10"
|
||||
onClick={() => onCancelQueued(commentId)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-3">
|
||||
|
|
@ -976,6 +1020,9 @@ function IssueChatAssistantMessage() {
|
|||
feedbackTermsUrl,
|
||||
onVote,
|
||||
agentMap,
|
||||
activeRunIds,
|
||||
onStopRun,
|
||||
stoppingRunId,
|
||||
} = useContext(IssueChatCtx);
|
||||
const message = useMessage();
|
||||
const custom = message.metadata.custom as Record<string, unknown>;
|
||||
|
|
@ -988,6 +1035,7 @@ function IssueChatAssistantMessage() {
|
|||
const authorAgentId = typeof custom.authorAgentId === "string" ? custom.authorAgentId : null;
|
||||
const runId = typeof custom.runId === "string" ? custom.runId : null;
|
||||
const runAgentId = typeof custom.runAgentId === "string" ? custom.runAgentId : null;
|
||||
const runStatus = typeof custom.runStatus === "string" ? custom.runStatus : null;
|
||||
const agentId = authorAgentId ?? runAgentId;
|
||||
const agentIcon = agentId ? agentMap?.get(agentId)?.icon : undefined;
|
||||
const commentId = typeof custom.commentId === "string" ? custom.commentId : null;
|
||||
|
|
@ -997,6 +1045,7 @@ function IssueChatAssistantMessage() {
|
|||
const waitingText = typeof custom.waitingText === "string" ? custom.waitingText : "";
|
||||
const isRunning = message.role === "assistant" && message.status?.type === "running";
|
||||
const runHref = runId && runAgentId ? `/agents/${runAgentId}/runs/${runId}` : null;
|
||||
const canStopRun = canStopIssueChatRun({ runId, runStatus, activeRunIds });
|
||||
const chainOfThoughtLabel = typeof custom.chainOfThoughtLabel === "string" ? custom.chainOfThoughtLabel : null;
|
||||
const hasCoT = message.content.some((p) => p.type === "reasoning" || p.type === "tool-call");
|
||||
const isFoldable = !isRunning && !!chainOfThoughtLabel;
|
||||
|
|
@ -1162,6 +1211,18 @@ function IssueChatAssistantMessage() {
|
|||
<Copy className="mr-2 h-3.5 w-3.5" />
|
||||
Copy message
|
||||
</DropdownMenuItem>
|
||||
{canStopRun && onStopRun && runId ? (
|
||||
<DropdownMenuItem
|
||||
disabled={stoppingRunId === runId}
|
||||
className="text-red-700 focus:text-red-800 dark:text-red-300 dark:focus:text-red-200"
|
||||
onSelect={() => {
|
||||
void onStopRun(runId);
|
||||
}}
|
||||
>
|
||||
<Square className="mr-2 h-3.5 w-3.5 fill-current" />
|
||||
{stoppingRunId === runId ? "Stopping…" : "Stop run"}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{runHref ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={runHref} target="_blank" rel="noreferrer noopener">
|
||||
|
|
@ -1557,7 +1618,6 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
}, forwardedRef) {
|
||||
const api = useAui();
|
||||
const [body, setBody] = useState("");
|
||||
const [reopen, setReopen] = useState(issueStatus === "done" || issueStatus === "cancelled");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [attaching, setAttaching] = useState(false);
|
||||
const effectiveSuggestedAssigneeValue = suggestedAssigneeValue ?? currentAssigneeValue;
|
||||
|
|
@ -1567,6 +1627,23 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
const composerContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function queueViewportRestore(snapshot: ReturnType<typeof captureComposerViewportSnapshot>) {
|
||||
if (!snapshot) return;
|
||||
requestAnimationFrame(() => {
|
||||
restoreComposerViewportSnapshot(snapshot, composerContainerRef.current);
|
||||
});
|
||||
}
|
||||
|
||||
function focusComposer() {
|
||||
if (typeof composerContainerRef.current?.scrollIntoView === "function") {
|
||||
composerContainerRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollBy({ top: COMPOSER_FOCUS_SCROLL_PADDING_PX, behavior: "smooth" });
|
||||
editorRef.current?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!draftKey) return;
|
||||
setBody(loadDraft(draftKey));
|
||||
|
|
@ -1591,12 +1668,15 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
}, [effectiveSuggestedAssigneeValue]);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
focus: () => {
|
||||
composerContainerRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollBy({ top: COMPOSER_FOCUS_SCROLL_PADDING_PX, behavior: "smooth" });
|
||||
editorRef.current?.focus();
|
||||
});
|
||||
focus: focusComposer,
|
||||
restoreDraft: (submittedBody: string) => {
|
||||
setBody((current) =>
|
||||
restoreSubmittedCommentDraft({
|
||||
currentBody: current,
|
||||
submittedBody,
|
||||
}),
|
||||
);
|
||||
focusComposer();
|
||||
},
|
||||
}), []);
|
||||
|
||||
|
|
@ -1606,12 +1686,17 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
|
||||
const hasReassignment = enableReassign && reassignTarget !== currentAssigneeValue;
|
||||
const reassignment = hasReassignment ? parseReassignment(reassignTarget) : undefined;
|
||||
const reopen = shouldImplicitlyReopenComment(
|
||||
issueStatus,
|
||||
hasReassignment ? reassignTarget : currentAssigneeValue,
|
||||
) ? true : undefined;
|
||||
const submittedBody = trimmed;
|
||||
const viewportSnapshot = captureComposerViewportSnapshot(composerContainerRef.current);
|
||||
|
||||
setSubmitting(true);
|
||||
setBody("");
|
||||
try {
|
||||
await api.thread().append({
|
||||
const appendPromise = api.thread().append({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: submittedBody }],
|
||||
metadata: { custom: {} },
|
||||
|
|
@ -1623,8 +1708,9 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
},
|
||||
},
|
||||
});
|
||||
queueViewportRestore(viewportSnapshot);
|
||||
await appendPromise;
|
||||
if (draftKey) clearDraft(draftKey);
|
||||
setReopen(issueStatus === "done" || issueStatus === "cancelled");
|
||||
setReassignTarget(effectiveSuggestedAssigneeValue);
|
||||
} catch {
|
||||
setBody((current) =>
|
||||
|
|
@ -1635,6 +1721,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
queueViewportRestore(viewportSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1707,16 +1794,6 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={reopen}
|
||||
onChange={(event) => setReopen(event.target.checked)}
|
||||
className="rounded border-border"
|
||||
/>
|
||||
Re-open
|
||||
</label>
|
||||
|
||||
{enableReassign && reassignOptions.length > 0 ? (
|
||||
<InlineEntitySelector
|
||||
value={reassignTarget}
|
||||
|
|
@ -1781,6 +1858,7 @@ export function IssueChatThread({
|
|||
onVote,
|
||||
onAdd,
|
||||
onCancelRun,
|
||||
onStopRun,
|
||||
imageUploadHandler,
|
||||
onAttachImage,
|
||||
draftKey,
|
||||
|
|
@ -1799,13 +1877,18 @@ export function IssueChatThread({
|
|||
hasOutputForRun: hasOutputForRunOverride,
|
||||
includeSucceededRunsWithoutOutput = false,
|
||||
onInterruptQueued,
|
||||
onCancelQueued,
|
||||
interruptingQueuedRunId = null,
|
||||
stoppingRunId = null,
|
||||
onImageClick,
|
||||
composerRef,
|
||||
}: IssueChatThreadProps) {
|
||||
const location = useLocation();
|
||||
const hasScrolledRef = useRef(false);
|
||||
const bottomAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerViewportAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerViewportSnapshotRef = useRef<ReturnType<typeof captureComposerViewportSnapshot>>(null);
|
||||
const preserveComposerViewportRef = useRef(false);
|
||||
const displayLiveRuns = useMemo(() => {
|
||||
const deduped = new Map<string, LiveRunForIssue>();
|
||||
for (const run of liveRuns) {
|
||||
|
|
@ -1834,14 +1917,22 @@ export function IssueChatThread({
|
|||
activeRun,
|
||||
});
|
||||
}, [activeRun, displayLiveRuns, linkedRuns]);
|
||||
const activeRunIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of displayLiveRuns) {
|
||||
if (run.status === "queued" || run.status === "running") {
|
||||
ids.add(run.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}, [displayLiveRuns]);
|
||||
const { transcriptByRun, hasOutputForRun } = useLiveRunTranscripts({
|
||||
runs: enableLiveTranscriptPolling ? transcriptRuns : [],
|
||||
companyId,
|
||||
});
|
||||
const resolvedTranscriptByRun = transcriptsByRunId ?? transcriptByRun;
|
||||
const resolvedHasOutputForRun = hasOutputForRunOverride ?? hasOutputForRun;
|
||||
|
||||
const messages = useMemo(
|
||||
const rawMessages = useMemo(
|
||||
() =>
|
||||
buildIssueChatMessages({
|
||||
comments,
|
||||
|
|
@ -1872,6 +1963,18 @@ export function IssueChatThread({
|
|||
currentUserId,
|
||||
],
|
||||
);
|
||||
const stableMessagesRef = useRef<readonly import("@assistant-ui/react").ThreadMessage[]>([]);
|
||||
const stableMessageCacheRef = useRef<Map<string, StableThreadMessageCacheEntry>>(new Map());
|
||||
const messages = useMemo(() => {
|
||||
const stabilized = stabilizeThreadMessages(
|
||||
rawMessages,
|
||||
stableMessagesRef.current,
|
||||
stableMessageCacheRef.current,
|
||||
);
|
||||
stableMessagesRef.current = stabilized.messages;
|
||||
stableMessageCacheRef.current = stabilized.cache;
|
||||
return stabilized.messages;
|
||||
}, [rawMessages]);
|
||||
|
||||
const isRunning = displayLiveRuns.some((run) => run.status === "queued" || run.status === "running");
|
||||
const feedbackVoteByTargetId = useMemo(() => {
|
||||
|
|
@ -1890,6 +1993,19 @@ export function IssueChatThread({
|
|||
onCancel: onCancelRun,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const composerElement = composerViewportAnchorRef.current;
|
||||
if (preserveComposerViewportRef.current) {
|
||||
restoreComposerViewportSnapshot(
|
||||
composerViewportSnapshotRef.current,
|
||||
composerElement,
|
||||
);
|
||||
}
|
||||
|
||||
composerViewportSnapshotRef.current = captureComposerViewportSnapshot(composerElement);
|
||||
preserveComposerViewportRef.current = shouldPreserveComposerViewport(composerElement);
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = location.hash;
|
||||
if (!(hash.startsWith("#comment-") || hash.startsWith("#activity-") || hash.startsWith("#run-"))) return;
|
||||
|
|
@ -1912,8 +2028,12 @@ export function IssueChatThread({
|
|||
feedbackTermsUrl,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
activeRunIds,
|
||||
onVote,
|
||||
onStopRun,
|
||||
stoppingRunId,
|
||||
onInterruptQueued,
|
||||
onCancelQueued,
|
||||
interruptingQueuedRunId,
|
||||
onImageClick,
|
||||
}),
|
||||
|
|
@ -1923,8 +2043,12 @@ export function IssueChatThread({
|
|||
feedbackTermsUrl,
|
||||
agentMap,
|
||||
currentUserId,
|
||||
activeRunIds,
|
||||
onVote,
|
||||
onStopRun,
|
||||
stoppingRunId,
|
||||
onInterruptQueued,
|
||||
onCancelQueued,
|
||||
interruptingQueuedRunId,
|
||||
onImageClick,
|
||||
],
|
||||
|
|
@ -1990,20 +2114,22 @@ export function IssueChatThread({
|
|||
</IssueChatErrorBoundary>
|
||||
|
||||
{showComposer ? (
|
||||
<IssueChatComposer
|
||||
ref={composerRef}
|
||||
onImageUpload={imageUploadHandler}
|
||||
onAttachImage={onAttachImage}
|
||||
draftKey={draftKey}
|
||||
enableReassign={enableReassign}
|
||||
reassignOptions={reassignOptions}
|
||||
currentAssigneeValue={currentAssigneeValue}
|
||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||
mentions={mentions}
|
||||
agentMap={agentMap}
|
||||
composerDisabledReason={composerDisabledReason}
|
||||
issueStatus={issueStatus}
|
||||
/>
|
||||
<div ref={composerViewportAnchorRef}>
|
||||
<IssueChatComposer
|
||||
ref={composerRef}
|
||||
onImageUpload={imageUploadHandler}
|
||||
onAttachImage={onAttachImage}
|
||||
draftKey={draftKey}
|
||||
enableReassign={enableReassign}
|
||||
reassignOptions={reassignOptions}
|
||||
currentAssigneeValue={currentAssigneeValue}
|
||||
suggestedAssigneeValue={suggestedAssigneeValue}
|
||||
mentions={mentions}
|
||||
agentMap={agentMap}
|
||||
composerDisabledReason={composerDisabledReason}
|
||||
issueStatus={issueStatus}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</IssueChatCtx.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue