Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function buildPrimaryNodes(options: LayoutNodesOptions): PrimaryLayoutNod
onPlanAccept={options.onPlanAccept}
onPlanSubmitChanges={options.onPlanSubmitChanges}
onOpenThreadLink={options.onOpenThreadLink}
onQuoteMessage={options.canInsertComposerText ? options.onInsertComposerText : undefined}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid draft-bound callback in quote prop wiring

This line passes options.onInsertComposerText directly into Messages, but that callback is created by useComposerInsert with draftText in its dependency list (src/features/app/hooks/useComposerInsert.ts), so its identity changes on every keystroke. As a result, Messages (and all markdown rows) re-render while typing even when message data is unchanged, which can noticeably degrade input responsiveness in long threads.

Useful? React with 👍 / 👎.

isThinking={options.isProcessing}
isLoadingMessages={
options.activeThreadId
Expand Down
14 changes: 14 additions & 0 deletions src/features/messages/components/MessageRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Diff from "lucide-react/dist/esm/icons/diff";
import FileDiff from "lucide-react/dist/esm/icons/file-diff";
import FileText from "lucide-react/dist/esm/icons/file-text";
import Image from "lucide-react/dist/esm/icons/image";
import Quote from "lucide-react/dist/esm/icons/quote";
import Search from "lucide-react/dist/esm/icons/search";
import Terminal from "lucide-react/dist/esm/icons/terminal";
import Users from "lucide-react/dist/esm/icons/users";
Expand Down Expand Up @@ -56,6 +57,7 @@ type MessageRowProps = MarkdownFileLinkProps & {
item: Extract<ConversationItem, { kind: "message" }>;
isCopied: boolean;
onCopy: (item: Extract<ConversationItem, { kind: "message" }>) => void;
onQuote?: (item: Extract<ConversationItem, { kind: "message" }>) => void;
codeBlockCopyUseModifier?: boolean;
};

Expand Down Expand Up @@ -360,6 +362,7 @@ export const MessageRow = memo(function MessageRow({
item,
isCopied,
onCopy,
onQuote,
codeBlockCopyUseModifier,
showMessageFilePath,
workspacePath,
Expand Down Expand Up @@ -414,6 +417,17 @@ export const MessageRow = memo(function MessageRow({
onClose={() => setLightboxIndex(null)}
/>
)}
{onQuote && hasText && (
<button
type="button"
className="ghost message-quote-button"
onClick={() => onQuote(item)}
aria-label="Quote message"
title="Quote message"
>
<Quote size={14} aria-hidden />
</button>
)}
<button
type="button"
className={`ghost message-copy-button${isCopied ? " is-copied" : ""}`}
Expand Down
27 changes: 27 additions & 0 deletions src/features/messages/components/Messages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,33 @@ describe("Messages", () => {
expect(markdown?.textContent ?? "").toContain("Literal [image] token");
});

it("quotes a message into composer using markdown blockquote format", () => {
const onQuoteMessage = vi.fn();
const items: ConversationItem[] = [
{
id: "msg-quote-1",
kind: "message",
role: "assistant",
text: "First line\nSecond line",
},
];

render(
<Messages
items={items}
threadId="thread-1"
workspaceId="ws-1"
isThinking={false}
openTargets={[]}
selectedOpenAppId=""
onQuoteMessage={onQuoteMessage}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "Quote message" }));
expect(onQuoteMessage).toHaveBeenCalledWith("> First line\n> Second line\n\n");
});

it("opens linked review thread when clicking thread link", () => {
const onOpenThreadLink = vi.fn();
const items: ConversationItem[] = [
Expand Down
29 changes: 29 additions & 0 deletions src/features/messages/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,21 @@ type MessagesProps = {
onPlanAccept?: () => void;
onPlanSubmitChanges?: (changes: string) => void;
onOpenThreadLink?: (threadId: string) => void;
onQuoteMessage?: (text: string) => void;
};

function toMarkdownQuote(text: string): string {
const trimmed = text.trim();
if (!trimmed) {
return "";
}
return trimmed
.split(/\r?\n/)
.map((line) => `> ${line}`)
.join("\n")
.concat("\n\n");
Comment on lines +71 to +75

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start generated quote blocks on a new line

The generated quote text always begins with > and has no leading newline, so when users quote into a non-empty draft the shared insert helper prefixes a space (not a line break) and the result becomes inline text like ... > quote instead of a Markdown blockquote. This means the feature fails to produce quote formatting unless the cursor is already at a line start.

Useful? React with 👍 / 👎.

}

export const Messages = memo(function Messages({
items,
threadId,
Expand All @@ -82,6 +95,7 @@ export const Messages = memo(function Messages({
onPlanAccept,
onPlanSubmitChanges,
onOpenThreadLink,
onQuoteMessage,
}: MessagesProps) {
const bottomRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -259,6 +273,20 @@ export const Messages = memo(function Messages({
[],
);

const handleQuoteMessage = useCallback(
(item: Extract<ConversationItem, { kind: "message" }>) => {
if (!onQuoteMessage) {
return;
}
const quoteText = toMarkdownQuote(item.text);
if (!quoteText) {
return;
}
onQuoteMessage(quoteText);
},
[onQuoteMessage],
);

useLayoutEffect(() => {
const container = containerRef.current;
const shouldScroll =
Expand Down Expand Up @@ -353,6 +381,7 @@ export const Messages = memo(function Messages({
item={item}
isCopied={isCopied}
onCopy={handleCopyMessage}
onQuote={onQuoteMessage ? handleQuoteMessage : undefined}
codeBlockCopyUseModifier={codeBlockCopyUseModifier}
showMessageFilePath={showMessageFilePath}
workspacePath={workspacePath}
Expand Down
19 changes: 18 additions & 1 deletion src/styles/messages.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,24 @@
transition: opacity 160ms ease, transform 160ms ease;
}

.message:hover .message-copy-button {
.message-quote-button {
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute;
right: 34px;
bottom: -12px;
padding: 4px;
border-radius: 999px;
background: var(--surface-card-strong);
border: 1px solid var(--border-strong);
opacity: 0;
transform: translateY(4px);
transition: opacity 160ms ease, transform 160ms ease;
}

.message:hover .message-copy-button,
.message:hover .message-quote-button {
opacity: 1;
transform: translateY(0);
}
Expand Down
Loading