mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Merge remote-tracking branch 'origin/master' into PAPA-1235-fix-ci
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
db75bc48ed
3 files changed
+98
-30
No files matched your search
@@ -68,10 +68,13 @@ describe("paperclip skill utils", () => {
|
||||
const skillPath = path.resolve(".agents/skills/create-issue-interaction-ui/SKILL.md");
|
||||
const skillBody = await fs.readFile(skillPath, "utf8");
|
||||
const normalizedSkillBody = skillBody.replace(/\s+/g, " ");
|
||||
const normalizedLowerSkillBody = normalizedSkillBody.toLowerCase();
|
||||
|
||||
expect(skillBody).toContain("name: create-issue-interaction-ui");
|
||||
expect(skillBody).toContain("Developer/maintainer skill");
|
||||
expect(normalizedSkillBody).toContain("Do NOT install this on production Paperclip agents");
|
||||
expect(normalizedLowerSkillBody).toContain("developer/maintainer skill");
|
||||
expect(normalizedLowerSkillBody).toContain(
|
||||
"not the operational agents that run inside a deployed paperclip company",
|
||||
);
|
||||
expect(skillBody).toContain("packages/shared/src/constants.ts");
|
||||
expect(skillBody).toContain("server/src/services/issue-thread-interactions.ts");
|
||||
expect(skillBody).toContain("ui/src/components/IssueThreadInteractionCard.tsx");
|
||||
|
||||
@@ -794,6 +794,7 @@ describe("IssueChatThread", () => {
|
||||
onAdd={async () => {}}
|
||||
showComposer={false}
|
||||
showJumpToLatest={false}
|
||||
autoScrollToHashOnInitialLoad
|
||||
enableLiveTranscriptPolling={false}
|
||||
transcriptsByRunId={issueChatLongThreadTranscriptsByRunId}
|
||||
hasOutputForRun={(runId) => issueChatLongThreadTranscriptsByRunId.has(runId)}
|
||||
@@ -891,7 +892,7 @@ describe("IssueChatThread", () => {
|
||||
requestAnimationFrameMock.mockRestore();
|
||||
});
|
||||
|
||||
it("scrolls loaded hash targets through the virtualized message index", () => {
|
||||
it("scrolls loaded hash targets through the virtualized message index when initial hash scrolling is enabled", () => {
|
||||
const root = createRoot(container);
|
||||
const targetComment = issueChatLongThreadComments.at(-1);
|
||||
expect(targetComment).toBeDefined();
|
||||
@@ -910,6 +911,7 @@ describe("IssueChatThread", () => {
|
||||
onAdd={async () => {}}
|
||||
showComposer={false}
|
||||
showJumpToLatest={false}
|
||||
autoScrollToHashOnInitialLoad
|
||||
enableLiveTranscriptPolling={false}
|
||||
transcriptsByRunId={issueChatLongThreadTranscriptsByRunId}
|
||||
hasOutputForRun={(runId) => issueChatLongThreadTranscriptsByRunId.has(runId)}
|
||||
@@ -1071,8 +1073,8 @@ describe("IssueChatThread", () => {
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(jump).toBeDefined();
|
||||
|
||||
// Flush the on-load auto-scroll-to-latest (PAP-97) so this test measures
|
||||
// only the jump-to-latest interaction, not the initial mount scroll.
|
||||
// Flush pending mount timers so this test measures only the explicit
|
||||
// jump-to-latest interaction.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
@@ -1101,10 +1103,9 @@ describe("IssueChatThread", () => {
|
||||
scrollHost.remove();
|
||||
});
|
||||
|
||||
// PAP-97: on first thread load we land on the latest comment instead of the
|
||||
// top of the thread (board rev-2 feedback for PAP-95). No deep-link hash and
|
||||
// no user interaction — the scroll must happen purely from mounting.
|
||||
it("auto-scrolls to the latest comment on initial load (PAP-97)", () => {
|
||||
// PAP-12003: initial page load must not jump to the latest comment. If a
|
||||
// user wants the newest message, the explicit Jump to latest control owns it.
|
||||
it("does not auto-scroll to the latest comment on initial load", () => {
|
||||
vi.useFakeTimers();
|
||||
container.remove();
|
||||
const scrollHost = document.createElement("main");
|
||||
@@ -1142,15 +1143,13 @@ describe("IssueChatThread", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// No jump click — let the mount auto-scroll's rAF + settle ticks run.
|
||||
// No jump click: initial render should preserve the page position.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
const scrolledToLatest =
|
||||
elementScrollToMock.mock.calls.some(([arg]) => hasSmoothScrollBehavior(arg))
|
||||
|| scrollIntoViewMock.mock.calls.length > 0;
|
||||
expect(scrolledToLatest).toBe(true);
|
||||
expect(elementScrollToMock).not.toHaveBeenCalled();
|
||||
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
||||
|
||||
Element.prototype.scrollIntoView = originalScrollIntoView;
|
||||
act(() => {
|
||||
@@ -1190,7 +1189,6 @@ describe("IssueChatThread", () => {
|
||||
agentMap={issueChatLongThreadAgentMap}
|
||||
currentUserId="user-board"
|
||||
onAdd={async () => {}}
|
||||
autoScrollToLatestOnInitialLoad={false}
|
||||
enableLiveTranscriptPolling={false}
|
||||
transcriptsByRunId={issueChatLongThreadTranscriptsByRunId}
|
||||
hasOutputForRun={(runId) => issueChatLongThreadTranscriptsByRunId.has(runId)}
|
||||
@@ -1228,6 +1226,60 @@ describe("IssueChatThread", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("can keep the page at the top on initial load even when the URL has a comment hash", () => {
|
||||
vi.useFakeTimers();
|
||||
const originalScrollIntoView = Element.prototype.scrollIntoView;
|
||||
const scrollIntoViewMock = vi.fn();
|
||||
Object.defineProperty(Element.prototype, "scrollIntoView", {
|
||||
configurable: true,
|
||||
value: scrollIntoViewMock,
|
||||
});
|
||||
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<MemoryRouter initialEntries={["/PAP/issues/PAP-12003#comment-comment-target"]}>
|
||||
<IssueChatThread
|
||||
comments={[{
|
||||
id: "comment-target",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
authorAgentId: "agent-1",
|
||||
authorUserId: null,
|
||||
authorType: "agent",
|
||||
body: "Previous done comment near the bottom.",
|
||||
presentation: null,
|
||||
metadata: null,
|
||||
createdAt: new Date("2026-07-07T21:13:07.902Z"),
|
||||
updatedAt: new Date("2026-07-07T21:13:07.902Z"),
|
||||
}]}
|
||||
linkedRuns={[]}
|
||||
timelineEvents={[]}
|
||||
liveRuns={[]}
|
||||
onAdd={async () => {}}
|
||||
showComposer={false}
|
||||
enableLiveTranscriptPolling={false}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(Element.prototype, "scrollIntoView", {
|
||||
configurable: true,
|
||||
value: originalScrollIntoView,
|
||||
});
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// Regression for PAP-2672: when the merged feed ends with a non-comment row
|
||||
// (run/timeline/embedded output) we still want Jump to latest to land on the
|
||||
// last comment, not whichever activity row sorts last.
|
||||
|
||||
@@ -481,6 +481,7 @@ interface IssueChatThreadProps {
|
||||
showComposer?: boolean;
|
||||
showJumpToLatest?: boolean;
|
||||
autoScrollToLatestOnInitialLoad?: boolean;
|
||||
autoScrollToHashOnInitialLoad?: boolean;
|
||||
emptyMessage?: string;
|
||||
footer?: ReactNode;
|
||||
variant?: "full" | "embedded";
|
||||
@@ -4216,7 +4217,8 @@ export function IssueChatThread({
|
||||
composerHint = null,
|
||||
showComposer = true,
|
||||
showJumpToLatest,
|
||||
autoScrollToLatestOnInitialLoad = true,
|
||||
autoScrollToLatestOnInitialLoad = false,
|
||||
autoScrollToHashOnInitialLoad = false,
|
||||
emptyMessage,
|
||||
footer,
|
||||
variant = "full",
|
||||
@@ -4246,6 +4248,7 @@ export function IssueChatThread({
|
||||
}: IssueChatThreadProps) {
|
||||
const location = useLocation();
|
||||
const lastScrolledHashRef = useRef<string | null>(null);
|
||||
const didInitialHashScrollDecisionRef = useRef(false);
|
||||
const virtualizedThreadRef = useRef<VirtualizedIssueChatThreadListHandle | null>(null);
|
||||
const bottomAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerViewportAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -4511,19 +4514,23 @@ export function IssueChatThread({
|
||||
|
||||
useEffect(() => {
|
||||
const hash = location.hash || (typeof window !== "undefined" ? window.location.hash : "");
|
||||
if (
|
||||
!(
|
||||
hash.startsWith("#comment-")
|
||||
|| hash.startsWith("#activity-")
|
||||
|| hash.startsWith("#run-")
|
||||
|| hash.startsWith("#interaction-")
|
||||
)
|
||||
) return;
|
||||
if (messages.length === 0 || lastScrolledHashRef.current === hash) return;
|
||||
const isThreadHash = hash.startsWith("#comment-")
|
||||
|| hash.startsWith("#activity-")
|
||||
|| hash.startsWith("#run-")
|
||||
|| hash.startsWith("#interaction-");
|
||||
if (messages.length === 0) return;
|
||||
if (!isThreadHash) {
|
||||
if (!didInitialHashScrollDecisionRef.current) {
|
||||
didInitialHashScrollDecisionRef.current = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lastScrolledHashRef.current === hash) return;
|
||||
const targetId = hash.slice(1);
|
||||
if (targetId.startsWith("comment-")) {
|
||||
const targetMessage = messages.find((message) => issueChatMessageAnchorId(message) === targetId);
|
||||
if (targetMessage && issueChatMessageIsDeleted(targetMessage)) {
|
||||
didInitialHashScrollDecisionRef.current = true;
|
||||
lastScrolledHashRef.current = hash;
|
||||
if (typeof window !== "undefined") {
|
||||
window.history.replaceState(null, "", `${location.pathname}${location.search}`);
|
||||
@@ -4531,6 +4538,13 @@ export function IssueChatThread({
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!didInitialHashScrollDecisionRef.current) {
|
||||
didInitialHashScrollDecisionRef.current = true;
|
||||
if (!autoScrollToHashOnInitialLoad) {
|
||||
lastScrolledHashRef.current = hash;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let cancelled = false;
|
||||
const attemptScroll = (finalAttempt = false) => {
|
||||
if (cancelled || lastScrolledHashRef.current === hash) return;
|
||||
@@ -4549,12 +4563,11 @@ export function IssueChatThread({
|
||||
cancelAnimationFrame(frame);
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [location.hash, messageAnchorIndex, messages, useVirtualizedThread]);
|
||||
}, [autoScrollToHashOnInitialLoad, location.hash, messageAnchorIndex, messages, useVirtualizedThread]);
|
||||
|
||||
// On first thread load, land on the latest comment instead of defaulting to
|
||||
// the top (board rev-2 feedback for PAP-95). A deep-link hash takes
|
||||
// precedence — the hash-scroll effect above owns that case. Runs once per
|
||||
// mount, after messages first populate.
|
||||
// Optional legacy behavior: callers may explicitly request landing on the
|
||||
// latest comment. The shared default stays off so ordinary page loads keep
|
||||
// the user's initial viewport stable.
|
||||
useEffect(() => {
|
||||
if (didInitialLatestScrollRef.current) return;
|
||||
if (!autoScrollToLatestOnInitialLoad) return;
|
||||
|
||||
Reference in new issue
Block a user