fix(less-computer): 全屏描边贴边修复(逻辑坐标+菜单栏层级)+苹果式高斯柔光(细带blur内化)+按下即亮; 弹框可拖动(顶栏拖把手+resize原位锚定); agent超时120→300s/预算0.5→2$

This commit is contained in:
吕柏青 committed 2026-06-05 21:12:48 +08:00
1 parent cf933cb347
commit 120cc152dc
5 files changed
+105 -49

No files matched your search

+24 -4
View File
@@ -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 {
+43 -7
View File
@@ -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}");
}
}
}
}
+26 -33
View File
@@ -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