feat(quick-pane): add follow-up chat renewal

This commit is contained in:
2026-08-27 16:26:17 +02:00
parent 05874a0be1
commit 60fb91f375
14 changed files with 1147 additions and 549 deletions
+34 -2
View File
@@ -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()];
+30 -8
View File
@@ -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");
+3 -3
View File
@@ -29,9 +29,9 @@
"label": "quick-pane",
"title": "Pi Quick Pane",
"width": 640,
"height": 440,
"minWidth": 400,
"minHeight": 260,
"height": 360,
"minWidth": 520,
"minHeight": 360,
"center": true,
"visible": false,
"decorations": false,