feat(app): 활성 창 캡쳐·캡쳐 목적지 선택·단축키 Ctrl+Alt 재배치 + UX 손질

- Ctrl+Alt+A/Z 맨 앞 창 통째 캡쳐(DWM 확장 프레임 경계), S/X 드래그 캡쳐. A/S 새 대화, Z/X 마지막 대화
- 캡쳐 후 창을 확실히 앞으로(AttachThreadInput), capture.image 에 목적지 path 실어 그 Composer 만 소비
- 드래그 선택영역 원본 밝기·더블버퍼(번쩍임) — code-assistant-v2 복사본에서 이식
- 단축키 Ctrl+Alt+Q/W: Eclipse·ADT·SAP GUI 바인딩 목록 대조해 비어 있는 조합으로
- 버튼 cursor:pointer(Tailwind v4), 가운데 스피너, 스니펫→챗 버튼, 스니펫 검색창 높이 84→88

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lee-hyeon-cheol
2026-09-17 16:34:48 +09:00
co-authored by Claude Fable 5.1
parent 2869985139
commit 47d31dbe06
27 changed files with 607 additions and 142 deletions
@@ -29,15 +29,20 @@ describe("bridgeNavigate", () => {
await emit("bridge", { type: "navigate", path: "/snippet" })
await emit("bridge", { type: "paste.target", name: "메모장", app: "notepad" })
await emit("bridge", { type: "capture.image", dataUrl: "data:image/png;base64,abc" })
await emit("bridge", {
type: "capture.image",
path: "/snap/new",
dataUrl: "data:image/png;base64,abc",
})
expect(navigate).toHaveBeenCalledWith("/snippet")
expect(onNavigate).toHaveBeenCalledTimes(1)
expect(getLastPasteTarget()).toEqual({ name: "메모장", app: "notepad" })
expect(onPasteTarget).toHaveBeenCalledTimes(1)
expect(onCapture).toHaveBeenCalledTimes(1)
expect(consumePendingCaptureImage()).toBe("data:image/png;base64,abc")
expect(consumePendingCaptureImage()).toBe("")
expect(consumePendingCaptureImage("/snap/s/old")).toBe("")
expect(consumePendingCaptureImage("/snap/new")).toBe("data:image/png;base64,abc")
expect(consumePendingCaptureImage("/snap/new")).toBe("")
window.removeEventListener("bridge:navigate", onNavigate)
window.removeEventListener("bridge:pasteTarget", onPasteTarget)
+9 -10
View File
@@ -9,7 +9,7 @@ export interface PasteTarget {
let navigateFn: NavigateFn | null = null
let lastPasteTarget: PasteTarget = { name: "", app: "" }
let pendingCaptureImage = ""
let pendingCaptureImage: { path: string; dataUrl: string } | null = null
/** 앱 루트에서 useNavigate() 감싼 콜백을 등록함. */
export function setBridgeNavigate(fn: NavigateFn | null) {
@@ -21,10 +21,11 @@ export function getLastPasteTarget(): PasteTarget {
return lastPasteTarget
}
/** 마지막 캡처 이미지를 한 번만 소비함. */
export function consumePendingCaptureImage(): string {
const dataUrl = pendingCaptureImage
pendingCaptureImage = ""
/** 지정한 대화에 도착한 마지막 캡처 이미지를 한 번만 소비함. */
export function consumePendingCaptureImage(path: string): string {
if (pendingCaptureImage?.path !== path) return ""
const { dataUrl } = pendingCaptureImage
pendingCaptureImage = null
return dataUrl
}
@@ -41,11 +42,9 @@ function handleBridgeMessage(data: {
return
}
if (data.type === "capture.image") {
pendingCaptureImage = data.dataUrl ?? ""
window.dispatchEvent(
new CustomEvent("bridge:captureImage", { detail: { dataUrl: pendingCaptureImage } })
)
if (data.type === "capture.image" && data.path) {
pendingCaptureImage = { path: data.path, dataUrl: data.dataUrl ?? "" }
window.dispatchEvent(new CustomEvent("bridge:captureImage", { detail: pendingCaptureImage }))
return
}