Skip to content

Commit 30a87cc

Browse files
committed
Chat improvements
Add OpenCode chat support, per-provider chat filtering, minor bug fixes
1 parent de0d908 commit 30a87cc

9 files changed

Lines changed: 457 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ anyhow = "1"
1515
dirs = "6"
1616
rfd = "0.15"
1717
zip = { version = "2", default-features = false }
18+
rusqlite = { version = "0.35", features = ["bundled"] }
1819

1920
[profile.release]
2021
opt-level = "z"

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Native desktop GUI for managing AI coding-agent configuration across providers.
99
- **Scope Switching** — project-level vs global configuration, with workspace browser.
1010
- **Diff Workbench** — compare project and global configs with stable, secret-safe fingerprints. Detects duplicates, missing targets, and scope conflicts.
1111
- **Hook Cockpit** — static hook inventory showing event, matcher, handler, blocking risk, timeout, duplicates, and project/global overlaps.
12-
- **Chat Manager** — unified chat history browser across Claude Code, Codex CLI, and Kiro. Search, export (single JSON or multi-chat ZIP), soft-delete with Trash, and import archived sessions.
12+
- **Chat Manager** — unified chat history browser across Claude Code, Codex CLI, Kiro, and OpenCode. Per-provider filtering when a provider is selected, or browse all providers together. Search, export (single JSON or multi-chat ZIP), soft-delete with Trash, and import archived sessions.
1313
- **Inline Editor** — edit instruction files, rules, and steering docs without leaving the app.
1414
- **JSON Backups** — automatic `.bak` creation before any config mutation.
1515
- **Cross-platform** — Windows, Linux, and macOS builds.
@@ -24,7 +24,7 @@ Native desktop GUI for managing AI coding-agent configuration across providers.
2424
| Codex CLI | `AGENTS.md` | `.codex/skills/`, `.agents/skills/` | `config.toml`, `hooks.json` | `config.toml`, `.mcp.json` ||
2525
| Antigravity CLI | `GEMINI.md`, `AGENTS.md` | `~/.gemini/skills/` ||||
2626
| Kiro ||| Agent JSON | `settings/mcp.json` | Steering, Specs, Agents |
27-
| OpenCode | `AGENTS.md` | `.opencode/skills/` | Plugins | `opencode.json` | Agents |
27+
| OpenCode | `AGENTS.md` | `.opencode/skills/` | Plugins | `opencode.json` | Agents, Chats (SQLite) |
2828

2929
## Install
3030

@@ -39,7 +39,7 @@ Download the matching binary from [Releases](https://github.com/RoyCoding8/Agent
3939

4040
## Build from Source
4141

42-
Requires the [Rust toolchain](https://rustup.rs/) (1.75+).
42+
Requires the [Rust toolchain](https://rustup.rs/) (1.75+). SQLite is bundled via `rusqlite` — no system dependency needed.
4343

4444
```bash
4545
git clone https://github.com/RoyCoding8/AgentSwitch.git
@@ -86,7 +86,7 @@ src/
8686
toggler.rs rename and JSON/TOML mutation logic
8787
diagnostics.rs project/global diff workbench engine
8888
hook_diag.rs static hook cockpit engine
89-
chat.rs chat history scanner, archive, export/import, trash
89+
chat.rs chat history scanner, archive, export/import, trash, OpenCode SQLite
9090
editor.rs inline markdown editor state
9191
ui/
9292
mod.rs module declarations

src/app.rs

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ impl App {
114114
}
115115

116116
fn rescan_chats(&mut self) {
117-
self.chat_sessions = chat::scan_all(&self.workspace);
117+
let filter = if self.view == View::Chats {
118+
self.selected_provider
119+
} else {
120+
None
121+
};
122+
self.chat_sessions = chat::scan_all(&self.workspace, filter);
118123
self.chat_trash = chat::scan_trash();
119124
let keys: HashSet<_> = self
120125
.chat_sessions
@@ -264,10 +269,14 @@ impl eframe::App for App {
264269
});
265270
ui_panel.add_space(4.0);
266271
ui_panel.horizontal(|ui| {
272+
let old_view = self.view;
267273
view_tab(ui, &mut self.view, View::Items, "Items");
268274
view_tab(ui, &mut self.view, View::Hooks, "Hooks");
269275
view_tab(ui, &mut self.view, View::Diff, "Diff");
270276
view_tab(ui, &mut self.view, View::Chats, "Chats");
277+
if self.view != old_view && self.view == View::Chats {
278+
self.rescan_chats();
279+
}
271280
if self.view == View::Items {
272281
let kinds = self.available_kinds();
273282
ui::item_list::filter_tabs(ui, &mut self.filter, &kinds);
@@ -317,22 +326,29 @@ impl eframe::App for App {
317326
.filter(|(_, it)| it.state.is_enabled() != want_enabled)
318327
.map(|(i, _)| i)
319328
.collect();
320-
let (mut ok, mut errs) = (0usize, vec![]);
321-
for idx in indices {
322-
match toggler::toggle_item(&mut self.items[idx]) {
323-
Ok(()) => ok += 1,
324-
Err(e) => errs.push(format!("{}: {e}", self.items[idx].name)),
329+
let mut toggled: Vec<usize> = Vec::new();
330+
let mut failed_name = None;
331+
for idx in &indices {
332+
match toggler::toggle_item(&mut self.items[*idx]) {
333+
Ok(()) => toggled.push(*idx),
334+
Err(e) => {
335+
failed_name = Some(format!("{}: {e}", self.items[*idx].name));
336+
break;
337+
}
325338
}
326339
}
327-
self.status_msg = Some(if errs.is_empty() {
328-
format!(
340+
if let Some(err) = failed_name {
341+
for &idx in toggled.iter().rev() {
342+
let _ = toggler::toggle_item(&mut self.items[idx]);
343+
}
344+
self.status_msg = Some(format!("Rolled back, error: {err}"));
345+
} else {
346+
self.status_msg = Some(format!(
329347
"{} items {}",
330-
ok,
348+
toggled.len(),
331349
if want_enabled { "enabled" } else { "disabled" }
332-
)
333-
} else {
334-
format!("{ok} ok, {} errors: {}", errs.len(), errs.join("; "))
335-
});
350+
));
351+
}
336352
self.rescan_items();
337353
} else if let Some(idx) = result.index {
338354
if idx < self.items.len() {
@@ -374,9 +390,15 @@ impl eframe::App for App {
374390
// refresh on scope/provider change
375391
if self.scope != old_scope {
376392
self.refresh();
393+
if self.view == View::Chats {
394+
self.rescan_chats();
395+
}
377396
} else if self.selected_provider != old_provider {
378397
self.rescan_items();
379398
self.filter = FilterKind::All;
399+
if self.view == View::Chats {
400+
self.rescan_chats();
401+
}
380402
}
381403
// Prevent being stuck in a view with no way to navigate away
382404
if self.selected_provider.is_none()
@@ -389,6 +411,10 @@ impl eframe::App for App {
389411

390412
impl App {
391413
fn show_chats(&mut self, ui_panel: &mut egui::Ui) {
414+
let provider_label = self
415+
.selected_provider
416+
.map(|p| p.label())
417+
.unwrap_or("All Providers");
392418
ui_panel.horizontal(|ui| {
393419
if ui.button("Back").clicked() {
394420
if self.selected_provider.is_none() {
@@ -400,14 +426,15 @@ impl App {
400426
}
401427
self.view = View::Items;
402428
}
429+
let title = if self.chat_trash_mode {
430+
format!("{} Chats Trash", provider_label)
431+
} else {
432+
format!("{} Chats", provider_label)
433+
};
403434
ui.label(
404-
egui::RichText::new(if self.chat_trash_mode {
405-
"Chats Trash"
406-
} else {
407-
"Chats"
408-
})
409-
.font(ui::theme::heading_font())
410-
.color(ui::theme::TEXT_PRIMARY),
435+
egui::RichText::new(title)
436+
.font(ui::theme::heading_font())
437+
.color(ui::theme::TEXT_PRIMARY),
411438
);
412439
});
413440
ui_panel.add_space(4.0);
@@ -514,11 +541,7 @@ impl App {
514541
let mut ok = 0usize;
515542
let mut failed = 0usize;
516543
for idx in action.trash {
517-
match self
518-
.chat_sessions
519-
.get(idx)
520-
.map(|session| chat::soft_delete(session, &self.workspace))
521-
{
544+
match self.chat_sessions.get(idx).map(chat::soft_delete) {
522545
Some(Ok(())) => ok += 1,
523546
_ => failed += 1,
524547
}

0 commit comments

Comments
 (0)