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
+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");