Skip to content

Commit f9438cb

Browse files
TestCopilot
andcommitted
fix(agent-input): make native drag bounds zoom-aware
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0bc7942 commit f9438cb

2 files changed

Lines changed: 129 additions & 8 deletions

File tree

packages/desktop/src/lib/acp/components/agent-input/state/__tests__/agent-input-state-drag-drop-listeners.vitest.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,30 @@ import type { SessionStore } from "../../../../store/session-store.svelte.js";
55

66
const listenMock = vi.fn();
77
const invokeMock = vi.fn();
8+
let zoomLevel = 0.8;
89

910
let AgentInputState: typeof import("../agent-input-state.svelte.js").AgentInputState;
1011

12+
interface DragPositionEvent {
13+
payload: {
14+
paths: string[];
15+
position: {
16+
x: number;
17+
y: number;
18+
};
19+
};
20+
}
21+
22+
function requireDragOverHandler(
23+
handler: ((event: DragPositionEvent) => void) | null
24+
): (event: DragPositionEvent) => void {
25+
if (handler === null) {
26+
throw new Error("Expected tauri://drag-over listener to register");
27+
}
28+
29+
return handler;
30+
}
31+
1132
function createPendingPromise<T>() {
1233
let resolveValue: ((value: T) => void) | null = null;
1334
const promise = new Promise<T>((resolve) => {
@@ -34,13 +55,19 @@ describe("AgentInputState drag-drop listener lifecycle", () => {
3455
beforeEach(async () => {
3556
listenMock.mockReset();
3657
invokeMock.mockReset();
58+
zoomLevel = 0.8;
3759

3860
mock.module("@tauri-apps/api/core", () => ({
3961
invoke: invokeMock,
4062
}));
4163
mock.module("@tauri-apps/api/event", () => ({
4264
listen: listenMock,
4365
}));
66+
mock.module("$lib/services/zoom.svelte.js", () => ({
67+
getZoomService: () => ({
68+
zoomLevel,
69+
}),
70+
}));
4471

4572
({ AgentInputState } = await import("../agent-input-state.svelte.js"));
4673
});
@@ -77,4 +104,90 @@ describe("AgentInputState drag-drop listener lifecycle", () => {
77104
expect(unlistenDrop).toHaveBeenCalledTimes(1);
78105
expect(unlistenLeave).toHaveBeenCalledTimes(1);
79106
});
80-
});
107+
108+
it("does not highlight the composer for native drag positions outside its zoomed bounds", async () => {
109+
let dragOverHandler: ((event: DragPositionEvent) => void) | null = null;
110+
111+
listenMock.mockImplementation((eventName: string, handler: ((event: DragPositionEvent) => void) | (() => void)) => {
112+
if (eventName === "tauri://drag-over") {
113+
dragOverHandler = handler as (event: DragPositionEvent) => void;
114+
}
115+
116+
return Promise.resolve(() => {});
117+
});
118+
119+
const state = new AgentInputState({} as SessionStore, {} as PanelStore);
120+
state.containerRef = {
121+
getBoundingClientRect: () => ({
122+
x: 100,
123+
y: 100,
124+
width: 100,
125+
height: 100,
126+
top: 100,
127+
right: 200,
128+
bottom: 200,
129+
left: 100,
130+
toJSON: () => ({}),
131+
}),
132+
} as HTMLElement;
133+
134+
state.initialize();
135+
await flushAsync();
136+
137+
expect(dragOverHandler).not.toBeNull();
138+
const registeredDragOverHandler = requireDragOverHandler(dragOverHandler);
139+
140+
registeredDragOverHandler({
141+
payload: {
142+
paths: ["/tmp/image.png"],
143+
position: { x: 170, y: 120 },
144+
},
145+
});
146+
147+
expect(state.isDragActive).toBe(true);
148+
expect(state.isDragHovering).toBe(false);
149+
});
150+
151+
it("does not highlight the composer for native drag positions just outside its bounds", async () => {
152+
let dragOverHandler: ((event: DragPositionEvent) => void) | null = null;
153+
zoomLevel = 1;
154+
155+
listenMock.mockImplementation((eventName: string, handler: ((event: DragPositionEvent) => void) | (() => void)) => {
156+
if (eventName === "tauri://drag-over") {
157+
dragOverHandler = handler as (event: DragPositionEvent) => void;
158+
}
159+
160+
return Promise.resolve(() => {});
161+
});
162+
163+
const state = new AgentInputState({} as SessionStore, {} as PanelStore);
164+
state.containerRef = {
165+
getBoundingClientRect: () => ({
166+
x: 100,
167+
y: 100,
168+
width: 100,
169+
height: 100,
170+
top: 100,
171+
right: 200,
172+
bottom: 200,
173+
left: 100,
174+
toJSON: () => ({}),
175+
}),
176+
} as HTMLElement;
177+
178+
state.initialize();
179+
await flushAsync();
180+
181+
expect(dragOverHandler).not.toBeNull();
182+
const registeredDragOverHandler = requireDragOverHandler(dragOverHandler);
183+
184+
registeredDragOverHandler({
185+
payload: {
186+
paths: ["/tmp/image.png"],
187+
position: { x: 201, y: 120 },
188+
},
189+
});
190+
191+
expect(state.isDragHovering).toBe(false);
192+
});
193+
});

packages/desktop/src/lib/acp/components/agent-input/state/agent-input-state.svelte.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
33
import { okAsync, Result, ResultAsync } from "neverthrow";
44
import { SvelteMap } from "svelte/reactivity";
55

6+
import { getZoomService } from "$lib/services/zoom.svelte.js";
67
import type { ProjectIndex } from "../../../../services/converted-session-types.js";
78
import { LOGGER_IDS } from "../../../constants/logger-ids.js";
89
import type { PanelStore } from "../../../store/panel-store.svelte.js";
@@ -256,18 +257,25 @@ export class AgentInputState {
256257

257258
/**
258259
* Checks if a position is within the container element's bounds.
259-
* Uses generous padding to handle any coordinate system offsets.
260+
* Native Tauri drag coordinates are reported in logical window pixels, so
261+
* the DOM rect must be scaled out of CSS pixels before hit-testing.
260262
*/
261263
private isPositionInBounds(position: { x: number; y: number }): boolean {
262264
if (!this.containerRef) return false;
263265
const rect = this.containerRef.getBoundingClientRect();
264-
// Add padding to be more forgiving with coordinate mismatches
265-
const padding = 50;
266+
const zoomLevel = getZoomService().zoomLevel;
267+
const normalizedZoomLevel =
268+
Number.isFinite(zoomLevel) && zoomLevel > 0 ? zoomLevel : 1;
269+
const left = rect.left * normalizedZoomLevel;
270+
const right = rect.right * normalizedZoomLevel;
271+
const top = rect.top * normalizedZoomLevel;
272+
const bottom = rect.bottom * normalizedZoomLevel;
273+
266274
return (
267-
position.x >= rect.left - padding &&
268-
position.x <= rect.right + padding &&
269-
position.y >= rect.top - padding &&
270-
position.y <= rect.bottom + padding
275+
position.x >= left &&
276+
position.x <= right &&
277+
position.y >= top &&
278+
position.y <= bottom
271279
);
272280
}
273281

0 commit comments

Comments
 (0)