fix(less-computer): 全屏描边贴边修复(逻辑坐标+菜单栏层级)+苹果式高斯柔光(细带blur内化)+按下即亮; 弹框可拖动(顶栏拖把手+resize原位锚定); agent超时120→300s/预算0.5→2$
This commit is contained in:
1 parent
cf933cb347
commit
120cc152dc
5 files changed
+105
-49
No files matched your search
@@ -1526,10 +1526,24 @@ async fn handle_less_computer_pressed(inner: &Arc<Inner>) {
|
||||
if begin_session(inner).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let mut state = inner.state.lock();
|
||||
if matches!(state.phase, SessionPhase::Starting | SessionPhase::Listening) {
|
||||
state.voice_agent = true;
|
||||
log::info!("[less-computer] voice session started (session={:?})", state.session_id);
|
||||
let started = {
|
||||
let mut state = inner.state.lock();
|
||||
if matches!(state.phase, SessionPhase::Starting | SessionPhase::Listening) {
|
||||
state.voice_agent = true;
|
||||
log::info!(
|
||||
"[less-computer] voice session started (session={:?})",
|
||||
state.session_id
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
// 一按下键(开始录音)就点亮整屏彩虹描边,贯穿 录音 → 处理 → 出结果,完成/关闭才熄灭。
|
||||
if started {
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::show_less_computer_glow(&app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1544,8 +1558,14 @@ async fn handle_less_computer_released(inner: &Arc<Inner>) {
|
||||
match phase {
|
||||
SessionPhase::Listening => {
|
||||
let _ = end_session(inner).await;
|
||||
// 收尾后熄灭整屏描边。正常路径 run_voice_agent_transcript 已熄过、这里兜底;
|
||||
// 空转写/出错路径不进 run_voice_agent_transcript,全靠这里熄,否则描边卡住不灭。
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::hide_less_computer_glow(&app);
|
||||
}
|
||||
}
|
||||
SessionPhase::Starting => {
|
||||
// 握手中松手:排队;真正收尾在 begin 续流的 end_session → run_voice_agent_transcript 熄灭。
|
||||
request_stop_during_starting(inner, "less-computer release edge");
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -594,7 +594,7 @@ async fn run_voice_agent_transcript(
|
||||
// 聊天浮窗:显示窗口 + 落用户气泡(语音指令转写)。macOS only(helper 内部 gating)。
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::show_less_computer_window(&app);
|
||||
crate::show_less_computer_glow(&app);
|
||||
// 全屏彩虹描边已在按下键时(handle_less_computer_pressed)点亮,这里不重复。
|
||||
}
|
||||
// 连续对话:浮窗里已有进行中的会话 → 本轮 `claude --continue` 续上下文;否则是新会话(fresh)。
|
||||
// dismiss 关窗会把标志复位为 false。
|
||||
@@ -764,8 +764,10 @@ async fn run_less_computer_once(
|
||||
"WebSearch".into(),
|
||||
];
|
||||
req.allowed_tools.extend(allow_rules);
|
||||
req.max_budget_usd = Some(0.5);
|
||||
req.timeout_secs = 120;
|
||||
// 真实任务(开应用、多步操作、读写文件)常超过 120s/0.5$ → 老是「运行超时」。放宽到
|
||||
// 5 分钟 / 2$,给多步任务足够空间;仍有硬上限兜底,不会无限跑/烧钱。
|
||||
req.max_budget_usd = Some(2.0);
|
||||
req.timeout_secs = 300;
|
||||
// 连续对话需要保留会话:本轮保存(供下轮 --continue),第二轮起带 --continue 续上下文。
|
||||
req.session_persistence = true;
|
||||
req.continue_session = continue_session;
|
||||
@@ -2291,6 +2293,10 @@ pub(super) fn cancel_session(inner: &Arc<Inner>) {
|
||||
emit_capsule(inner, CapsuleState::Cancelled, 0.0, 0, None, None);
|
||||
log::info!("[coord] session cancelled (was {:?})", decision.phase);
|
||||
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);
|
||||
// 取消时也熄灭整屏彩虹描边(dictation session 没开描边,hide 是无害 no-op)。
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::hide_less_computer_glow(&app);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_typed_prefix(target: &mut String, delta: &str, typed_chars: usize) -> usize {
|
||||
|
||||
@@ -1384,15 +1384,25 @@ pub(crate) fn show_less_computer_glow<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
let Some(window) = app.get_webview_window("less-computer-glow") else {
|
||||
return;
|
||||
};
|
||||
// 盖满当前(否则主)显示器,含菜单栏/Dock 区域 —— 屏幕边缘环绕发光。
|
||||
// 盖满当前(否则主)显示器,含菜单栏/Dock 区域。关键:用「逻辑坐标」(物理/缩放) ——
|
||||
// Retina 上 monitor.size() 是物理像素(2x),直接 set_size 会把窗口铺成两倍、错位、不贴边。
|
||||
let monitor = window
|
||||
.current_monitor()
|
||||
.ok()
|
||||
.flatten()
|
||||
.or_else(|| app.primary_monitor().ok().flatten());
|
||||
if let Some(monitor) = monitor {
|
||||
let _ = window.set_position(*monitor.position());
|
||||
let _ = window.set_size(*monitor.size());
|
||||
let scale = monitor.scale_factor();
|
||||
let size = monitor.size();
|
||||
let pos = monitor.position();
|
||||
let _ = window.set_position(tauri::LogicalPosition::new(
|
||||
pos.x as f64 / scale,
|
||||
pos.y as f64 / scale,
|
||||
));
|
||||
let _ = window.set_size(tauri::LogicalSize::new(
|
||||
size.width as f64 / scale,
|
||||
size.height as f64 / scale,
|
||||
));
|
||||
}
|
||||
// 点击穿透:纯视觉浮层,绝不拦截鼠标。
|
||||
let _ = window.set_ignore_cursor_events(true);
|
||||
@@ -1407,6 +1417,11 @@ pub(crate) fn show_less_computer_glow<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
let _ = window_clone.show();
|
||||
} else {
|
||||
unsafe {
|
||||
// 抬到菜单栏(24)/Dock 之上,让描边能真正贴到屏幕最外缘(含顶部菜单栏区域)。
|
||||
let _: () = msg_send![ns, setLevel: 25i64];
|
||||
// 所有 Space 都显示、不参与窗口循环、全屏 app 上也叠加。
|
||||
let _: () = msg_send![ns, setCollectionBehavior: 273u64];
|
||||
let _: () = msg_send![ns, setIgnoresMouseEvents: true];
|
||||
let _: () = msg_send![ns, orderFrontRegardless];
|
||||
}
|
||||
}
|
||||
@@ -1432,12 +1447,33 @@ pub(crate) fn hide_less_computer_glow<R: tauri::Runtime>(app: &AppHandle<R>) {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) fn hide_less_computer_glow<R: tauri::Runtime>(_app: &AppHandle<R>) {}
|
||||
|
||||
/// 前端按内容测高后回传,Rust 端 clamp + bottom-anchored 重新摆放。`macos` 专用。
|
||||
/// 前端按内容测高后回传。以「当前窗口底边」为锚向上生长——只改高度、保住用户拖动后的位置,
|
||||
/// 不再重新居中(否则一改内容就把拖走的框拉回屏幕底部中间)。`macos` 专用。
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn resize_less_computer_window<R: tauri::Runtime>(app: &AppHandle<R>, height: f64) {
|
||||
if let Some(window) = app.get_webview_window("less-computer") {
|
||||
if let Err(e) = position_less_computer_window(&window, height) {
|
||||
log::warn!("[less-computer] resize failed: {e}");
|
||||
let Some(window) = app.get_webview_window("less-computer") else {
|
||||
return;
|
||||
};
|
||||
let height = height.clamp(
|
||||
LESS_COMPUTER_WINDOW_MIN_HEIGHT,
|
||||
LESS_COMPUTER_WINDOW_MAX_HEIGHT,
|
||||
);
|
||||
let scale = window.scale_factor().unwrap_or(1.0);
|
||||
match (window.outer_position(), window.outer_size()) {
|
||||
(Ok(pos), Ok(size)) => {
|
||||
let x = pos.x as f64 / scale;
|
||||
let cur_top = pos.y as f64 / scale;
|
||||
let cur_h = size.height as f64 / scale;
|
||||
let bottom = cur_top + cur_h;
|
||||
let new_y = (bottom - height).max(0.0);
|
||||
let _ = window.set_size(tauri::LogicalSize::new(LESS_COMPUTER_WINDOW_WIDTH, height));
|
||||
let _ = window.set_position(tauri::LogicalPosition::new(x, new_y));
|
||||
}
|
||||
// 拿不到当前位置(极少见)→ 退回首屏居中摆放。
|
||||
_ => {
|
||||
if let Err(e) = position_less_computer_window(&window, height) {
|
||||
log::warn!("[less-computer] resize fallback failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
// Less Computer 全屏彩虹「跑马灯」描边浮层(独立窗口 window=less-computer-glow)。
|
||||
//
|
||||
// 思路(参考开源 MacEdgeLight / Apple Edge Light):一个全屏、点击穿透、置顶、透明的
|
||||
// 覆盖窗,沿屏幕圆角画一圈旋转的彩虹环 + 柔光,营造「整屏被 AI 点亮」的氛围感。
|
||||
// 纯视觉:pointer-events:none,且后端对该窗口 set_ignore_cursor_events(true)。
|
||||
// 仅 macOS 实际显示(后端只在 macOS show/hide 这个窗口)。
|
||||
// 思路(参考开源 MacEdgeLight / Apple Edge Light,自写实现):一个全屏、点击穿透、置顶、
|
||||
// 透明的覆盖窗,沿屏幕边缘画一圈「贴边清晰、向内高斯模糊羽化」的流动彩虹辉光。
|
||||
// - 细带 conic-gradient 彩虹环,首尾同色;
|
||||
// - 重 blur() 高斯模糊 → 既把环糊成柔光、又彻底抹平 conic 起始角的断层;
|
||||
// - 容器 overflow:hidden 把外溢的模糊裁在屏幕最外缘 → 边缘清晰、内部模糊(苹果那种感觉);
|
||||
// - 旋转 = 跑马灯流动,呼吸 = 明暗起伏。
|
||||
// 纯视觉:pointer-events:none,后端再 set_ignore_cursor_events(true)。仅 macOS 显示。
|
||||
|
||||
const glowCss = `
|
||||
@property --lcg-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
|
||||
@keyframes lcg-spin { to { --lcg-angle: 360deg; } }
|
||||
@keyframes lcg-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
|
||||
@keyframes lcg-glow-breathe { 0%, 100% { opacity: .35; } 50% { opacity: .7; } }
|
||||
@keyframes lcg-breathe { 0%, 100% { opacity: .58; } 50% { opacity: 1; } }
|
||||
|
||||
html, body, #root { background: transparent !important; margin: 0; height: 100%; overflow: hidden; }
|
||||
|
||||
/* 旋转的彩虹环:conic-gradient 首尾同色(无断层)→ mask 抠出一圈描边 → 绕 --lcg-angle 旋转。 */
|
||||
.lcg-ring {
|
||||
/* 全屏裁剪容器:圆角贴合屏幕物理圆角;overflow:hidden 把外溢模糊裁在屏幕边缘。 */
|
||||
.lcg-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: var(--lcg-radius, 40px);
|
||||
padding: var(--lcg-width, 5px);
|
||||
overflow: hidden;
|
||||
border-radius: var(--lcg-radius, 34px);
|
||||
}
|
||||
/* 彩虹环:略微跨出屏幕边缘(inset 负) → 满色部分压在最外缘(清晰),blur 向内羽化(模糊)。
|
||||
细带(padding) + 重 blur = 苹果 Edge Light 的柔光,而不是一条又粗又硬的描边。 */
|
||||
.lcg-root::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -5px;
|
||||
border-radius: calc(var(--lcg-radius, 34px) + 5px);
|
||||
padding: 9px;
|
||||
background: conic-gradient(from var(--lcg-angle),
|
||||
#ff2d78, #ff8a00, #ffd400, #38e08a, #00c2ff, #7a5cff, #ff2d78);
|
||||
#ff3b6b, #ff8a3d, #ffe14d, #46e08a, #36c6ff, #9b6bff, #ff3b6b);
|
||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
animation: lcg-spin 6s linear infinite, lcg-breathe 5s ease-in-out infinite;
|
||||
filter: saturate(1.15) blur(0.4px);
|
||||
}
|
||||
|
||||
/* 柔光层:贴着屏幕边往内渗的同色辉光(体积感),让描边不是一条硬线而是「发光」。 */
|
||||
.lcg-soft {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: var(--lcg-radius, 40px);
|
||||
box-shadow:
|
||||
inset 0 0 38px 4px rgba(124, 92, 255, 0.20),
|
||||
inset 0 0 90px 18px rgba(0, 194, 255, 0.10);
|
||||
animation: lcg-glow-breathe 5s ease-in-out infinite;
|
||||
filter: blur(22px) saturate(1.3);
|
||||
animation: lcg-spin 6s linear infinite, lcg-breathe 4.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lcg-ring { animation: none; opacity: .8; }
|
||||
.lcg-soft { animation: none; }
|
||||
.lcg-root::before { animation: none; opacity: .85; }
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -56,10 +54,5 @@ if (typeof document !== 'undefined' && !document.getElementById('less-computer-g
|
||||
}
|
||||
|
||||
export function LessComputerGlow() {
|
||||
return (
|
||||
<>
|
||||
<div className="lcg-soft" aria-hidden />
|
||||
<div className="lcg-ring" aria-hidden />
|
||||
</>
|
||||
);
|
||||
return <div className="lcg-root" aria-hidden />;
|
||||
}
|
||||
@@ -247,8 +247,9 @@ function TurnView({
|
||||
|
||||
function Toolbar({ label, onClose }: { label: string; onClose: () => void }) {
|
||||
return (
|
||||
<div style={toolbarStyle}>
|
||||
<div style={{ flex: 1 }} />
|
||||
// 顶栏作为拖动把手:按住空白处可把整个聊天框拖到屏幕任意位置(resize 会保住拖后的位置)。
|
||||
<div data-tauri-drag-region style={{ ...toolbarStyle, cursor: 'grab' }}>
|
||||
<div data-tauri-drag-region style={{ flex: 1, alignSelf: 'stretch' }} />
|
||||
<button onClick={onClose} title={label} aria-label={label} style={closeBtnStyle}>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11">
|
||||
<path
|
||||
|
||||
Reference in new issue
Block a user