feat(quick-pane): add follow-up chat renewal
This commit is contained in:
+34
-2
@@ -9,7 +9,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{
|
||||
async_runtime::JoinHandle, AppHandle, Emitter, Manager, State, WebviewWindow,
|
||||
async_runtime::JoinHandle, AppHandle, Emitter, LogicalSize, Manager, State, WebviewWindow,
|
||||
WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
|
||||
@@ -283,6 +283,28 @@ async fn get_model_catalog() -> Result<Value, String> {
|
||||
bridge::get_model_catalog(&socket_path()?).await
|
||||
}
|
||||
|
||||
fn quick_pane_height(height: f64) -> f64 {
|
||||
height.clamp(360.0, 520.0)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn resize_quick_pane(app: AppHandle, height: f64) -> Result<(), String> {
|
||||
if !height.is_finite() {
|
||||
return Err("Quick Pane height must be finite".to_owned());
|
||||
}
|
||||
let quick_pane = app
|
||||
.get_webview_window("quick-pane")
|
||||
.ok_or_else(|| "Quick Pane window is unavailable".to_owned())?;
|
||||
let scale_factor = quick_pane.scale_factor().map_err(|error| error.to_string())?;
|
||||
let current_size = quick_pane.inner_size().map_err(|error| error.to_string())?;
|
||||
quick_pane
|
||||
.set_size(LogicalSize::new(
|
||||
f64::from(current_size.width) / scale_factor,
|
||||
quick_pane_height(height),
|
||||
))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn dismiss_quick_pane(
|
||||
app: AppHandle,
|
||||
@@ -601,6 +623,7 @@ pub fn run() {
|
||||
get_workspace,
|
||||
get_workspace_summary,
|
||||
get_model_catalog,
|
||||
resize_quick_pane,
|
||||
dismiss_quick_pane,
|
||||
create_session_runtime,
|
||||
create_quick_runtime,
|
||||
@@ -767,7 +790,9 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(quick_pane.width, 640.0);
|
||||
assert_eq!(quick_pane.height, 440.0);
|
||||
assert_eq!(quick_pane.height, 360.0);
|
||||
assert_eq!(quick_pane.min_width, Some(520.0));
|
||||
assert_eq!(quick_pane.min_height, Some(360.0));
|
||||
assert_eq!(quick_pane.url.to_string(), "index.html");
|
||||
assert!(!quick_pane.visible);
|
||||
assert!(quick_pane.transparent);
|
||||
@@ -775,6 +800,13 @@ mod tests {
|
||||
assert!(quick_pane.skip_taskbar);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_pane_height_stays_within_content_limit() {
|
||||
assert_eq!(quick_pane_height(200.0), 360.0);
|
||||
assert_eq!(quick_pane_height(420.0), 420.0);
|
||||
assert_eq!(quick_pane_height(900.0), 520.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_hides_a_visible_window_and_shows_a_hidden_window() {
|
||||
let toggle = vec!["--toggle".to_owned()];
|
||||
|
||||
@@ -46,7 +46,6 @@ pub struct QuickPaneSettings {
|
||||
pub follow_up_provider: String,
|
||||
pub follow_up_model: String,
|
||||
pub instructions: String,
|
||||
pub maximum_seconds: u32,
|
||||
pub maximum_tool_calls: u32,
|
||||
}
|
||||
|
||||
@@ -58,8 +57,7 @@ impl Default for QuickPaneSettings {
|
||||
model: String::new(),
|
||||
follow_up_provider: String::new(),
|
||||
follow_up_model: String::new(),
|
||||
instructions: "Answer quickly. Do not use subagents. Prefer read-only inspection. Escalate mutations, multi-step work, or work likely to exceed budget.".to_owned(),
|
||||
maximum_seconds: 60,
|
||||
instructions: "Answer quickly. Do not use subagents. Prefer read-only inspection. Escalate mutations, multi-step work, or complex work.".to_owned(),
|
||||
maximum_tool_calls: 6,
|
||||
}
|
||||
}
|
||||
@@ -232,9 +230,8 @@ pub fn validate(state: &UiStateV1) -> Result<(), String> {
|
||||
if quick.instructions.is_empty() || quick.instructions.len() > 4096 {
|
||||
return Err("Quick-pane instructions must contain 1 to 4096 bytes".to_owned());
|
||||
}
|
||||
if !(1..=600).contains(&quick.maximum_seconds) || !(1..=64).contains(&quick.maximum_tool_calls)
|
||||
{
|
||||
return Err("Quick-pane limits are outside the supported range".to_owned());
|
||||
if !(1..=64).contains(&quick.maximum_tool_calls) {
|
||||
return Err("Quick-pane tool-call limit is outside the supported range".to_owned());
|
||||
}
|
||||
total_string_bytes = total_string_bytes
|
||||
.saturating_add(quick.workspace.len())
|
||||
@@ -281,7 +278,16 @@ pub fn load_from(app_data_dir: &Path) -> Result<UiStateV1, String> {
|
||||
if content.len() > MAX_FILE_BYTES {
|
||||
return Err("UI state file exceeds the size limit".to_owned());
|
||||
}
|
||||
let state: UiStateV1 = serde_json::from_slice(&content)
|
||||
let mut value: serde_json::Value = serde_json::from_slice(&content)
|
||||
.map_err(|error| format!("UI state is corrupt or unsupported: {error}"))?;
|
||||
// Read legacy persisted cap once, but never retain or serialize it again.
|
||||
if let Some(quick_pane) = value
|
||||
.get_mut("quickPane")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
quick_pane.remove("maximumSeconds");
|
||||
}
|
||||
let state: UiStateV1 = serde_json::from_value(value)
|
||||
.map_err(|error| format!("UI state is corrupt or unsupported: {error}"))?;
|
||||
validate(&state)?;
|
||||
Ok(state)
|
||||
@@ -443,11 +449,27 @@ mod tests {
|
||||
state.quick_pane.follow_up_provider = "provider".into();
|
||||
state.quick_pane.follow_up_model = "follow-up-model".into();
|
||||
assert!(validate(&state).is_ok());
|
||||
state.quick_pane.maximum_seconds = 0;
|
||||
state.quick_pane.maximum_tool_calls = 0;
|
||||
assert!(validate(&state).is_err());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_maximum_seconds_loads_but_is_not_serialized() {
|
||||
let root = temp_dir("quick-pane-legacy-seconds");
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
let path = state_path(&root);
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"version":1,"quickPane":{"workspace":"","provider":"","model":"","followUpProvider":"","followUpModel":"","instructions":"valid","maximumSeconds":60,"maximumToolCalls":6}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let state = load_from(&root).unwrap();
|
||||
save_to(&root, &state).unwrap();
|
||||
assert!(!fs::read_to_string(&path).unwrap().contains("maximumSeconds"));
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_save_replaces_valid_state_without_leaving_temporary_files() {
|
||||
let root = temp_dir("replace");
|
||||
|
||||
Reference in New Issue
Block a user