Skip to content

Commit 7994f9b

Browse files
committed
compact: extract shared helpers, deduplicate hook scanning, toggle, and path collision logic
- Extract str_field and parse_json_or_string to types.rs (removed 3 duplicates) - Merge near-identical hooks + _agentswitch_disabled blocks in scan_hook_entries - Extract apply_json_toggle helper in toggler.rs - Extract unique_path helper in chat.rs (4 collision loops → 1) - All 15 tests pass, zero behavioral change
1 parent 5782fcc commit 7994f9b

6 files changed

Lines changed: 128 additions & 127 deletions

File tree

src/chat.rs

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::types::ProviderId;
1+
use crate::types::{str_field, ProviderId};
22
use anyhow::{anyhow, Result};
33
use serde::{Deserialize, Serialize};
44
use serde_json::Value;
@@ -275,16 +275,33 @@ pub fn import_archive(path: &Path, project_dir: Option<&Path>) -> Result<PathBuf
275275
archive.source_provider.id(),
276276
archive.title
277277
));
278-
let mut target = dir.join(format!("{base}.{ARCHIVE_EXT}"));
279-
let mut n = 2usize;
280-
while target.exists() {
281-
target = dir.join(format!("{base}-{n}.{ARCHIVE_EXT}"));
282-
n += 1;
283-
}
278+
let target = unique_path(&dir, &base, ARCHIVE_EXT);
284279
fs::write(&target, serde_json::to_string_pretty(&archive)?)?;
285280
Ok(target)
286281
}
287282

283+
fn unique_path(dir: &Path, stem: &str, ext: &str) -> PathBuf {
284+
let first = if ext.is_empty() {
285+
dir.join(stem)
286+
} else {
287+
dir.join(format!("{stem}.{ext}"))
288+
};
289+
if !first.exists() {
290+
return first;
291+
}
292+
for n in 2.. {
293+
let candidate = if ext.is_empty() {
294+
dir.join(format!("{stem}-{n}"))
295+
} else {
296+
dir.join(format!("{stem}-{n}.{ext}"))
297+
};
298+
if !candidate.exists() {
299+
return candidate;
300+
}
301+
}
302+
unreachable!()
303+
}
304+
288305
pub fn import_zip(path: &Path, project_dir: Option<&Path>) -> Result<BatchReport> {
289306
let file = File::open(path)?;
290307
let mut zip = ZipArchive::new(file)?;
@@ -318,12 +335,7 @@ pub fn import_zip(path: &Path, project_dir: Option<&Path>) -> Result<BatchReport
318335
archive.source_provider.id(),
319336
archive.title
320337
));
321-
let mut target = dir.join(format!("{base}.{ARCHIVE_EXT}"));
322-
let mut n = 2usize;
323-
while target.exists() {
324-
target = dir.join(format!("{base}-{n}.{ARCHIVE_EXT}"));
325-
n += 1;
326-
}
338+
let target = unique_path(&dir, &base, ARCHIVE_EXT);
327339
fs::write(&target, serde_json::to_string_pretty(&archive)?)?;
328340
report.ok += 1;
329341
}
@@ -351,12 +363,7 @@ pub fn soft_delete(session: &ChatSession, workspace: &Path) -> Result<()> {
351363
.extension()
352364
.and_then(|e| e.to_str())
353365
.unwrap_or("chat");
354-
let mut target = trash_dir.join(format!("{stem}.{ext}"));
355-
let mut n = 2usize;
356-
while target.exists() {
357-
target = trash_dir.join(format!("{stem}-{n}.{ext}"));
358-
n += 1;
359-
}
366+
let target = unique_path(&trash_dir, &stem, ext);
360367
move_path(source, &target)?;
361368
let manifest = DeleteManifest {
362369
schema_version: ARCHIVE_VERSION,
@@ -390,12 +397,7 @@ fn soft_delete_kiro_session(session: &ChatSession) -> Result<()> {
390397
let trash_dir = trash_dir().join(session.provider.id());
391398
fs::create_dir_all(&trash_dir)?;
392399
let stem = safe_file_stem(&format!("{}-{}", session.id, session.title));
393-
let mut target = trash_dir.join(&stem);
394-
let mut n = 2usize;
395-
while target.exists() {
396-
target = trash_dir.join(format!("{stem}-{n}"));
397-
n += 1;
398-
}
400+
let target = unique_path(&trash_dir, &stem, "");
399401
fs::create_dir_all(&target)?;
400402
let base = meta_path.with_extension("");
401403
for ext in ["json", "jsonl", "lock"] {
@@ -1177,11 +1179,6 @@ fn project_label_from_path(provider: ChatProvider, _root: &Path, path: &Path) ->
11771179
}
11781180
}
11791181

1180-
fn str_field<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
1181-
keys.iter()
1182-
.find_map(|key| value.get(*key).and_then(Value::as_str))
1183-
}
1184-
11851182
fn first_string(value: Option<&Value>) -> Option<&str> {
11861183
value
11871184
.and_then(Value::as_array)

src/diagnostics.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::{scanner, types::*};
1+
use crate::{
2+
scanner,
3+
types::{parse_json_or_string as parse_detail, str_field, *},
4+
};
25
use serde_json::Value;
36
use std::{
47
collections::BTreeMap,
@@ -219,10 +222,6 @@ fn item_key(item: &ConfigItem) -> String {
219222
item.name.clone()
220223
}
221224

222-
fn parse_detail(raw: &str) -> Value {
223-
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.into()))
224-
}
225-
226225
fn fingerprint(value: &Value) -> String {
227226
serde_json::to_string(&canonical(value, None, true)).unwrap_or_else(|_| value.to_string())
228227
}
@@ -340,11 +339,6 @@ fn hook_handler(value: &Value) -> Option<&str> {
340339
.or_else(|| str_field(value, &["name", "command", "cmd"]))
341340
}
342341

343-
fn str_field<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
344-
keys.iter()
345-
.find_map(|k| value.get(*k).and_then(|v| v.as_str()))
346-
}
347-
348342
fn object_len(value: &Value, key: &str) -> Option<usize> {
349343
value.get(key).and_then(|v| v.as_object()).map(|o| o.len())
350344
}

src/hook_diag.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::{scanner, types::*};
1+
use crate::{
2+
scanner,
3+
types::{str_field, *},
4+
};
25
use serde_json::Value;
36
use std::{
47
collections::HashMap,
@@ -103,7 +106,7 @@ pub fn build(provider: ProviderId, workspace: &Path) -> Vec<HookRow> {
103106
}
104107

105108
fn row(item: ConfigItem, scope: Scope) -> HookRow {
106-
let value = parse(item.detail.as_deref().unwrap_or(""));
109+
let value = parse_json_or_string(item.detail.as_deref().unwrap_or(""));
107110
let loc = item.hook_loc.clone().unwrap_or(HookLoc {
108111
event: "hook".into(),
109112
index: 0,
@@ -259,12 +262,8 @@ fn timeout(value: &Value) -> Option<u64> {
259262
.or_else(|| num_field(value, &["timeout", "timeout_ms", "timeoutMs"]))
260263
}
261264

262-
fn parse(raw: &str) -> Value {
263-
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.into()))
264-
}
265-
fn str_field<'a>(v: &'a Value, keys: &[&str]) -> Option<&'a str> {
266-
keys.iter().find_map(|k| v.get(*k).and_then(|v| v.as_str()))
267-
}
265+
use crate::types::parse_json_or_string;
266+
268267
fn num_field(v: &Value, keys: &[&str]) -> Option<u64> {
269268
keys.iter().find_map(|k| v.get(*k).and_then(|v| v.as_u64()))
270269
}

src/scanner.rs

Lines changed: 50 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -302,29 +302,16 @@ fn scan_toml_hooks(path: &Path, provider: ProviderId) -> Vec<ConfigItem> {
302302
out
303303
}
304304

305-
fn scan_hook_entries(
305+
fn collect_hook_items(
306+
entries_iter: impl Iterator<Item = (String, serde_json::Value)>,
306307
path: &Path,
307308
provider: ProviderId,
308309
disabled_names: &[String],
310+
event_prefix: &str,
311+
force_disabled: bool,
309312
) -> Vec<ConfigItem> {
310313
let mut out = vec![];
311-
let text = match std::fs::read_to_string(path) {
312-
Ok(t) => t,
313-
_ => return out,
314-
};
315-
let doc: serde_json::Value = match serde_json::from_str(&text) {
316-
Ok(d) => d,
317-
_ => return out,
318-
};
319-
let hooks_obj = match doc.get("hooks").and_then(|v| v.as_object()) {
320-
Some(o) => o,
321-
_ => return out,
322-
};
323-
324-
for (event, entries) in hooks_obj {
325-
if event == "disabled" || event.starts_with("_agentswitch") {
326-
continue;
327-
}
314+
for (event, entries) in entries_iter {
328315
let arr = match entries.as_array() {
329316
Some(a) => a,
330317
_ => continue,
@@ -340,11 +327,12 @@ fn scan_hook_entries(
340327
let display = hook_name
341328
.clone()
342329
.unwrap_or_else(|| format!("{}: {}", event, matcher));
343-
let is_disabled = hook_name
344-
.as_ref()
345-
.is_some_and(|n| disabled_names.contains(n));
330+
let is_disabled = force_disabled
331+
|| hook_name
332+
.as_ref()
333+
.is_some_and(|n| disabled_names.contains(n));
346334
let loc = HookLoc {
347-
event: event.clone(),
335+
event: format!("{}{}", event_prefix, event),
348336
index: i,
349337
hook_name: hook_name.unwrap_or_else(|| matcher.to_string()),
350338
};
@@ -358,36 +346,47 @@ fn scan_hook_entries(
358346
out.push(item);
359347
}
360348
}
349+
out
350+
}
351+
352+
fn scan_hook_entries(
353+
path: &Path,
354+
provider: ProviderId,
355+
disabled_names: &[String],
356+
) -> Vec<ConfigItem> {
357+
let mut out = vec![];
358+
let text = match std::fs::read_to_string(path) {
359+
Ok(t) => t,
360+
_ => return out,
361+
};
362+
let doc: serde_json::Value = match serde_json::from_str(&text) {
363+
Ok(d) => d,
364+
_ => return out,
365+
};
366+
if let Some(hooks_obj) = doc.get("hooks").and_then(|v| v.as_object()) {
367+
let filtered = hooks_obj
368+
.iter()
369+
.filter(|(event, _)| event.as_str() != "disabled" && !event.starts_with("_agentswitch"))
370+
.map(|(e, v)| (e.clone(), v.clone()));
371+
out.extend(collect_hook_items(
372+
filtered,
373+
path,
374+
provider,
375+
disabled_names,
376+
"",
377+
false,
378+
));
379+
}
361380
if let Some(stashed) = doc.get("_agentswitch_disabled").and_then(|v| v.as_object()) {
362-
for (event, entries) in stashed {
363-
let arr = match entries.as_array() {
364-
Some(a) => a,
365-
_ => continue,
366-
};
367-
for (i, entry) in arr.iter().enumerate() {
368-
let matcher = entry.get("matcher").and_then(|v| v.as_str()).unwrap_or("*");
369-
let hook_name = entry
370-
.get("hooks")
371-
.and_then(|h| h.as_array())
372-
.and_then(|a| a.first())
373-
.and_then(|h| h.get("name").and_then(|n| n.as_str()))
374-
.map(String::from);
375-
let display = hook_name
376-
.clone()
377-
.unwrap_or_else(|| format!("{}: {}", event, matcher));
378-
let loc = HookLoc {
379-
event: format!("_stashed_{}", event),
380-
index: i,
381-
hook_name: hook_name.unwrap_or_else(|| matcher.to_string()),
382-
};
383-
let mut item = ConfigItem::new(display, ItemKind::Hook, path.to_owned(), provider);
384-
item.hook_loc = Some(loc);
385-
item.state = ItemState::Disabled;
386-
item.editable = false;
387-
item.detail = Some(json_detail(entry));
388-
out.push(item);
389-
}
390-
}
381+
let mapped = stashed.iter().map(|(e, v)| (e.clone(), v.clone()));
382+
out.extend(collect_hook_items(
383+
mapped,
384+
path,
385+
provider,
386+
&[],
387+
"_stashed_",
388+
true,
389+
));
391390
}
392391
out
393392
}

src/toggler.rs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -160,31 +160,41 @@ fn toggle_toml_mcp(item: &mut ConfigItem) -> Result<()> {
160160
Ok(())
161161
}
162162

163+
/// Set enable/disable markers on a JSON object. For "mcpServers" uses
164+
/// a "disabled" bool; for "mcp"/"agent" uses an "enabled" bool.
165+
fn apply_json_toggle(
166+
o: &mut serde_json::Map<String, serde_json::Value>,
167+
section: &str,
168+
enable: bool,
169+
) {
170+
if section == "mcpServers" {
171+
if enable {
172+
o.remove("disabled");
173+
} else {
174+
o.insert("disabled".into(), serde_json::Value::Bool(true));
175+
}
176+
} else {
177+
o.insert("enabled".into(), serde_json::Value::Bool(enable));
178+
}
179+
}
180+
163181
fn toggle_json_item(item: &mut ConfigItem) -> Result<()> {
164182
backup(&item.path)?;
165183
let mut doc: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&item.path)?)?;
184+
let enable = !item.state.is_enabled();
166185

167186
let candidates = ["mcpServers", "mcp", "agent"];
168187
let mut found = false;
169188
for key in candidates {
170189
if let Some(obj) = doc.get_mut(key).and_then(|v| v.as_object_mut()) {
171190
if let Some(val) = obj.get_mut(&item.name) {
172191
if let Some(o) = val.as_object_mut() {
173-
if item.state.is_enabled() {
174-
if key == "mcp" || key == "agent" {
175-
o.insert("enabled".to_string(), serde_json::Value::Bool(false));
176-
} else {
177-
o.insert("disabled".to_string(), serde_json::Value::Bool(true));
178-
}
179-
item.state = ItemState::Disabled;
192+
apply_json_toggle(o, key, enable);
193+
item.state = if enable {
194+
ItemState::Enabled
180195
} else {
181-
if key == "mcp" || key == "agent" {
182-
o.insert("enabled".to_string(), serde_json::Value::Bool(true));
183-
} else {
184-
o.remove("disabled");
185-
}
186-
item.state = ItemState::Enabled;
187-
}
196+
ItemState::Disabled
197+
};
188198
found = true;
189199
break;
190200
}
@@ -195,21 +205,12 @@ fn toggle_json_item(item: &mut ConfigItem) -> Result<()> {
195205
if let Some(obj) = doc.get_mut(&disabled_key).and_then(|v| v.as_object_mut()) {
196206
if let Some(mut val) = obj.remove(&item.name) {
197207
if let Some(o) = val.as_object_mut() {
198-
if item.state.is_enabled() {
199-
if key == "mcp" || key == "agent" {
200-
o.insert("enabled".to_string(), serde_json::Value::Bool(false));
201-
} else {
202-
o.insert("disabled".to_string(), serde_json::Value::Bool(true));
203-
}
204-
item.state = ItemState::Disabled;
208+
apply_json_toggle(o, key, enable);
209+
item.state = if enable {
210+
ItemState::Enabled
205211
} else {
206-
if key == "mcp" || key == "agent" {
207-
o.insert("enabled".to_string(), serde_json::Value::Bool(true));
208-
} else {
209-
o.remove("disabled");
210-
}
211-
item.state = ItemState::Enabled;
212-
}
212+
ItemState::Disabled
213+
};
213214
}
214215
let main_obj = doc
215216
.as_object_mut()

src/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,14 @@ pub enum FilterKind {
176176
All,
177177
Specific(ItemKind),
178178
}
179+
180+
/// Look up the first matching key in a JSON value and return it as &str.
181+
pub fn str_field<'a>(value: &'a serde_json::Value, keys: &[&str]) -> Option<&'a str> {
182+
keys.iter()
183+
.find_map(|key| value.get(*key).and_then(|v| v.as_str()))
184+
}
185+
186+
/// Parse a string as JSON, falling back to a JSON string literal.
187+
pub fn parse_json_or_string(raw: &str) -> serde_json::Value {
188+
serde_json::from_str(raw).unwrap_or_else(|_| serde_json::Value::String(raw.into()))
189+
}

0 commit comments

Comments
 (0)