Add issue detail shortcut for comment composer

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-08 09:24:32 -05:00
parent 15b0f11275
commit 1079f21ac4
6 changed files with 164 additions and 44 deletions

View file

@ -1,12 +1,16 @@
// @vitest-environment jsdom
import { act } from "react";
import { act, createRef, forwardRef, useImperativeHandle } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IssueChatThread, resolveAssistantMessageFoldedState } from "./IssueChatThread";
const { markdownEditorFocusMock } = vi.hoisted(() => ({
markdownEditorFocusMock: vi.fn(),
}));
vi.mock("@assistant-ui/react", () => ({
AssistantRuntimeProvider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
ThreadPrimitive: {
@ -48,7 +52,7 @@ vi.mock("./MarkdownBody", () => ({
}));
vi.mock("./MarkdownEditor", () => ({
MarkdownEditor: ({
MarkdownEditor: forwardRef(({
value = "",
onChange,
placeholder,
@ -60,16 +64,22 @@ vi.mock("./MarkdownEditor", () => ({
placeholder?: string;
className?: string;
contentClassName?: string;
}) => (
<textarea
aria-label="Issue chat editor"
data-class-name={className}
data-content-class-name={contentClassName}
placeholder={placeholder}
value={value}
onChange={(event) => onChange?.(event.target.value)}
/>
),
}, ref) => {
useImperativeHandle(ref, () => ({
focus: markdownEditorFocusMock,
}));
return (
<textarea
aria-label="Issue chat editor"
data-class-name={className}
data-content-class-name={contentClassName}
placeholder={placeholder}
value={value}
onChange={(event) => onChange?.(event.target.value)}
/>
);
}),
}));
vi.mock("./InlineEntitySelector", () => ({
@ -111,6 +121,7 @@ describe("IssueChatThread", () => {
afterEach(() => {
container.remove();
vi.useRealTimers();
markdownEditorFocusMock.mockReset();
});
it("drops the count heading and does not use an internal scrollbox", () => {
@ -279,6 +290,52 @@ describe("IssueChatThread", () => {
});
});
it("exposes a composer focus handle that forwards to the editor", () => {
const root = createRoot(container);
const composerRef = createRef<{ focus: () => void }>();
const requestAnimationFrameMock = vi
.spyOn(window, "requestAnimationFrame")
.mockImplementation((callback: FrameRequestCallback) => {
callback(0);
return 1;
});
act(() => {
root.render(
<MemoryRouter>
<IssueChatThread
comments={[]}
linkedRuns={[]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
composerRef={composerRef}
enableLiveTranscriptPolling={false}
/>
</MemoryRouter>,
);
});
const composer = container.querySelector('[data-testid="issue-chat-composer"]') as HTMLDivElement | null;
expect(composerRef.current).not.toBeNull();
expect(composer).not.toBeNull();
const scrollIntoViewMock = vi.fn();
composer!.scrollIntoView = scrollIntoViewMock;
act(() => {
composerRef.current?.focus();
});
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: "smooth", block: "end" });
expect(markdownEditorFocusMock).toHaveBeenCalledTimes(1);
requestAnimationFrameMock.mockRestore();
act(() => {
root.unmount();
});
});
it("folds chain-of-thought when the same message transitions from running to complete", () => {
expect(resolveAssistantMessageFoldedState({
messageId: "message-1",