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
+80 -19
View File
@@ -18,15 +18,29 @@ use shell::window;
const HOTKEYS: &[HotkeySpec] = &[
HotkeySpec {
id: "snap",
accelerator: "Ctrl+Shift+8",
accelerator: "Ctrl+Alt+W",
},
HotkeySpec {
id: "snippet",
accelerator: "Ctrl+Shift+7",
accelerator: "Ctrl+Alt+Q",
},
// 캡쳐 4종: 윗줄(A/S) = 새 대화, 아랫줄(Z/X) = 마지막 기존 대화(없으면 새 대화).
// 왼쪽(A/Z) = 맨 앞 창 통째, 오른쪽(S/X) = 드래그 영역.
HotkeySpec {
id: "capture",
accelerator: "Ctrl+Shift+9",
accelerator: "Ctrl+Alt+A",
},
HotkeySpec {
id: "capture_area",
accelerator: "Ctrl+Alt+S",
},
HotkeySpec {
id: "capture_last",
accelerator: "Ctrl+Alt+Z",
},
HotkeySpec {
id: "capture_area_last",
accelerator: "Ctrl+Alt+X",
},
];
@@ -149,6 +163,35 @@ fn show_palette(app: &AppHandle) -> tauri::Result<()> {
Ok(())
}
/// 캡쳐 핫키 id 로 목적지 route 결정. `*_last` 는 마지막 기존 대화, 없으면 새 대화.
fn capture_target(id: &str, last_session: Option<&str>) -> String {
if id.ends_with("_last") {
last_session.unwrap_or("/snap/new").to_string()
} else {
"/snap/new".to_string()
}
}
/// 캡쳐 결과 공통 처리 — 팔레트 소환 → 목적지 대화 화면 → 그 화면 Composer 에 이미지 첨부.
fn on_captured(app: AppHandle, target: String, data_url: String) {
if let Err(error) = show_palette(&app) {
eprintln!("[capture] 창 소환 실패: {error}");
}
bridge::push(
&app,
Push::Navigate {
path: target.clone(),
},
);
bridge::push(
&app,
Push::CaptureImage {
path: target,
data_url,
},
);
}
/// 표준 전역 단축키가 실행할 CodeAssist 동작을 한 곳에서 정함.
fn on_hotkey(app: &AppHandle, id: &str) {
match id {
@@ -162,17 +205,20 @@ fn on_hotkey(app: &AppHandle, id: &str) {
);
}
"capture" => {
shell::capture::start(app.clone(), |app, data_url| {
let _ = show_palette(&app);
bridge::push(
&app,
Push::Navigate {
path: "/snap/new".into(),
},
);
bridge::push(&app, Push::CaptureImage { data_url });
});
"capture" | "capture_area" | "capture_last" | "capture_area_last" => {
let target = app
.state::<commands::RouteState>()
.lock()
.map(|routes| capture_target(id, routes.last_session.as_deref()))
.unwrap_or_else(|_| "/snap/new".to_string());
let deliver =
move |app: AppHandle, data_url: String| on_captured(app, target, data_url);
if id.starts_with("capture_area") {
shell::capture::start(app.clone(), deliver);
} else {
let own_hwnd = window::native_handle(app);
shell::capture::start_foreground_window(app.clone(), own_hwnd, deliver);
}
}
// 챗봇 토글: 이미 챗봇 화면이 떠 있으면 숨기고, 아니면 마지막 챗봇 위치로 데려온다.
@@ -199,7 +245,7 @@ fn on_hotkey(app: &AppHandle, id: &str) {
#[cfg(test)]
mod tests {
use super::{chat_toggle_target, commands::Routes};
use super::{capture_target, chat_toggle_target, commands::Routes};
#[test]
fn _토글은_현재_화면과_창_표시에_따라_결정한다() {
@@ -219,13 +265,28 @@ mod tests {
}
#[test]
fn _챗봇_위치는_챗봇_화면에서만_바뀐() {
fn _기존_대화는_새_대화나_다른_화면으로_가도_유지된() {
let mut routes = Routes::default();
routes.report("/snap/session-1");
routes.report("/snippet");
routes.report("/snap/s/session-1");
routes.report("/snap/new");
routes.report("/files");
assert_eq!(routes.current, "/files");
assert_eq!(routes.last_snap, "/snap/session-1");
assert_eq!(routes.last_snap, "/snap/new");
assert_eq!(routes.last_session.as_deref(), Some("/snap/s/session-1"));
}
#[test]
fn _핫키에_따라_새_대화나_마지막_기존_대화를_고른다() {
assert_eq!(capture_target("capture", Some("/snap/s/last")), "/snap/new");
assert_eq!(
capture_target("capture_area", Some("/snap/s/last")),
"/snap/new"
);
assert_eq!(
capture_target("capture_last", Some("/snap/s/last")),
"/snap/s/last"
);
assert_eq!(capture_target("capture_area_last", None), "/snap/new");
}
}