Skip to content

Commit 7fec611

Browse files
sangggggclaude
andauthored
Add GUI support for preset provider imports (#99)
* feat: add GUI for preset provider imports Add graphical interface for importing chat sessions from preset provider directories, complementing existing file-based import functionality. Backend changes: - Add import_from_provider Tauri command supporting all/claude/gemini/codex - Implement ImportStats struct for cleaner parameter management - Integrate with existing ImportService and provider configs - Support batch importing from multiple provider directories Frontend changes: - Add import method selection dialog with file and preset options - Create provider selection UI with All/Claude/Gemini/Codex cards - Add toast notifications for import progress and results - Auto-refresh session list after successful imports Technical notes: - Passes cargo clippy -- -D warnings - Passes pnpm biome:check - Follows existing architecture patterns Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: address code review feedback for preset import GUI Improvements based on code review: 1. Concurrency Control: - Add isImporting state to prevent concurrent imports - Show warning toast if import attempted while one is in progress - Disable all import buttons during import operation 2. Enhanced Error Messages: - Show detailed error information for failed imports - Display first 3 failed files with their error messages - Add "+N more" indicator if more than 3 failures 3. Accessibility: - Add descriptive ARIA labels to all import buttons - Labels describe what each button does for screen readers - Include provider directory paths in labels 4. User Experience: - Buttons visually disabled during import (opacity + cursor) - Better error context for debugging issues - More informative toast notifications Technical notes: - Passes pnpm biome:check - All functionality tested and verified Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: simplify error messages to show only success count Remove verbose error details from toast notifications. Now shows: 'Imported 5/10 files (50 sessions, 200 messages)' instead of listing individual file errors. Error details are still logged to console for debugging. * refactor: simplify dialog animation to fade only Remove slide and zoom animations from dialog. Now uses simple opacity fade for cleaner appearance. Before: Dialog slid in from top-left with zoom After: Dialog fades in at center position * revert: restore default shadcn Dialog animations Keep the standard shadcn Dialog component with original animations. The default behavior works well for most use cases. * refactor: customize Dialog to use fade-only animation Remove slide and zoom animations from Dialog component. Now uses simple opacity fade for cleaner UX. Changes: - Removed slide-in/out animations - Removed zoom-in/out effects - Kept fade-in/out for smooth transitions * refactor: slow down Dialog animation to reduce flashing Increase animation duration from 200ms to 300ms. Makes the fade transition smoother and less jarring. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9cfcbb5 commit 7fec611

File tree

6 files changed

+406
-13
lines changed

6 files changed

+406
-13
lines changed

src-tauri/src/commands/file.rs

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::dto::{ImportFileResult, ImportSessionsResponse};
22
use crate::{AppState, OpenedFiles};
3-
use retrochat::services::ImportFileRequest;
3+
use retrochat::models::provider::config::{ClaudeCodeConfig, CodexConfig, GeminiCliConfig};
4+
use retrochat::models::Provider;
5+
use retrochat::services::{BatchImportRequest, ImportFileRequest};
46
use std::path::PathBuf;
57
use std::sync::Arc;
68
use tauri::{AppHandle, Emitter, Manager, State};
@@ -144,3 +146,202 @@ pub async fn import_sessions(
144146
results,
145147
})
146148
}
149+
150+
// Helper struct to track import stats
151+
struct ImportStats {
152+
results: Vec<ImportFileResult>,
153+
total_sessions_imported: i32,
154+
total_messages_imported: i32,
155+
successful_imports: i32,
156+
failed_imports: i32,
157+
total_files: i32,
158+
}
159+
160+
impl ImportStats {
161+
fn new() -> Self {
162+
Self {
163+
results: Vec::new(),
164+
total_sessions_imported: 0,
165+
total_messages_imported: 0,
166+
successful_imports: 0,
167+
failed_imports: 0,
168+
total_files: 0,
169+
}
170+
}
171+
}
172+
173+
// Command to import sessions from preset providers
174+
#[tauri::command]
175+
pub async fn import_from_provider(
176+
state: State<'_, Arc<Mutex<AppState>>>,
177+
provider: String,
178+
overwrite: bool,
179+
) -> Result<ImportSessionsResponse, String> {
180+
log::info!(
181+
"import_from_provider called with provider: {}, overwrite: {}",
182+
provider,
183+
overwrite
184+
);
185+
186+
let state_guard = state.lock().await;
187+
let import_service = &state_guard.import_service;
188+
189+
let mut stats = ImportStats::new();
190+
191+
// Parse provider string to Provider enum
192+
let providers = match provider.to_lowercase().as_str() {
193+
"all" => vec![Provider::All],
194+
"claude" => vec![Provider::ClaudeCode],
195+
"gemini" => vec![Provider::GeminiCLI],
196+
"codex" => vec![Provider::Codex],
197+
_ => return Err(format!("Unknown provider: {}", provider)),
198+
};
199+
200+
// Expand "All" to all specific providers
201+
let expanded_providers = Provider::expand_all(providers);
202+
203+
for prov in expanded_providers {
204+
match prov {
205+
Provider::All => {
206+
// Should not happen due to expansion above
207+
unreachable!("Provider::All should have been expanded")
208+
}
209+
Provider::ClaudeCode => {
210+
log::info!("Importing from Claude Code directories...");
211+
if let Err(e) = import_provider_directories(
212+
&ClaudeCodeConfig::create(),
213+
import_service,
214+
overwrite,
215+
&mut stats,
216+
)
217+
.await
218+
{
219+
log::error!("Error importing Claude directories: {}", e);
220+
}
221+
}
222+
Provider::GeminiCLI => {
223+
log::info!("Importing from Gemini directories...");
224+
if let Err(e) = import_provider_directories(
225+
&GeminiCliConfig::create(),
226+
import_service,
227+
overwrite,
228+
&mut stats,
229+
)
230+
.await
231+
{
232+
log::error!("Error importing Gemini directories: {}", e);
233+
}
234+
}
235+
Provider::Codex => {
236+
log::info!("Importing from Codex directories...");
237+
if let Err(e) = import_provider_directories(
238+
&CodexConfig::create(),
239+
import_service,
240+
overwrite,
241+
&mut stats,
242+
)
243+
.await
244+
{
245+
log::error!("Error importing Codex directories: {}", e);
246+
}
247+
}
248+
Provider::Other(name) => {
249+
log::error!("Unknown provider: {}", name);
250+
return Err(format!("Unknown provider: {}", name));
251+
}
252+
}
253+
}
254+
255+
log::info!(
256+
"Provider import completed - {} successful, {} failed, total: {} sessions, {} messages",
257+
stats.successful_imports,
258+
stats.failed_imports,
259+
stats.total_sessions_imported,
260+
stats.total_messages_imported
261+
);
262+
263+
Ok(ImportSessionsResponse {
264+
total_files: stats.total_files,
265+
successful_imports: stats.successful_imports,
266+
failed_imports: stats.failed_imports,
267+
total_sessions_imported: stats.total_sessions_imported,
268+
total_messages_imported: stats.total_messages_imported,
269+
results: stats.results,
270+
})
271+
}
272+
273+
// Helper function to import from a provider's directories
274+
async fn import_provider_directories(
275+
config: &retrochat::models::provider::config::ProviderConfig,
276+
import_service: &retrochat::services::ImportService,
277+
overwrite: bool,
278+
stats: &mut ImportStats,
279+
) -> Result<(), String> {
280+
let directories = config.get_import_directories();
281+
282+
if directories.is_empty() {
283+
log::info!("No directories found for provider: {}", config.name);
284+
return Ok(());
285+
}
286+
287+
for dir_path in directories {
288+
let path = std::path::Path::new(&dir_path);
289+
if !path.exists() {
290+
log::warn!("Directory not found: {}", path.display());
291+
continue;
292+
}
293+
294+
log::info!("Importing from directory: {}", path.display());
295+
296+
let batch_request = BatchImportRequest {
297+
directory_path: dir_path.clone(),
298+
providers: None,
299+
project_name: None,
300+
overwrite_existing: Some(overwrite),
301+
recursive: Some(true),
302+
};
303+
304+
match import_service.import_batch(batch_request).await {
305+
Ok(response) => {
306+
log::info!(
307+
"Successfully imported from directory '{}': {} sessions, {} messages",
308+
dir_path,
309+
response.total_sessions_imported,
310+
response.total_messages_imported
311+
);
312+
313+
stats.total_files += response.total_files_processed;
314+
stats.successful_imports += response.successful_imports;
315+
stats.failed_imports += response.failed_imports;
316+
stats.total_sessions_imported += response.total_sessions_imported;
317+
stats.total_messages_imported += response.total_messages_imported;
318+
319+
// Add directory-level result
320+
stats.results.push(ImportFileResult {
321+
file_path: dir_path.clone(),
322+
sessions_imported: response.total_sessions_imported,
323+
messages_imported: response.total_messages_imported,
324+
success: response.failed_imports == 0,
325+
error: if response.errors.is_empty() {
326+
None
327+
} else {
328+
Some(response.errors.join("; "))
329+
},
330+
});
331+
}
332+
Err(e) => {
333+
log::error!("Failed to import from directory '{}': {}", dir_path, e);
334+
stats.failed_imports += 1;
335+
stats.results.push(ImportFileResult {
336+
file_path: dir_path.clone(),
337+
sessions_imported: 0,
338+
messages_imported: 0,
339+
success: false,
340+
error: Some(e.to_string()),
341+
});
342+
}
343+
}
344+
}
345+
346+
Ok(())
347+
}

src-tauri/src/commands/session.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,8 @@ pub async fn get_session_detail(
122122

123123
// Create a map of tool_operation_id -> tool_operation for efficient lookup
124124
log::debug!("Building tool operation lookup map");
125-
let tool_op_by_id: std::collections::HashMap<_, _> = tool_operations
126-
.into_iter()
127-
.map(|op| (op.id, op))
128-
.collect();
125+
let tool_op_by_id: std::collections::HashMap<_, _> =
126+
tool_operations.into_iter().map(|op| (op.id, op)).collect();
129127

130128
// Create a map of message_id -> tool_operation
131129
log::debug!("Building message -> tool operation map");

src-tauri/src/dto.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -370,15 +370,15 @@ impl From<retrochat::services::analytics::ToolUsageMetrics> for ToolUsageMetrics
370370

371371
#[derive(Debug, Serialize, Deserialize)]
372372
pub struct HistogramRequest {
373-
pub start_time: String, // RFC3339 timestamp
374-
pub end_time: String, // RFC3339 timestamp
375-
pub interval_minutes: i32, // 5, 15, 60, 360
373+
pub start_time: String, // RFC3339 timestamp
374+
pub end_time: String, // RFC3339 timestamp
375+
pub interval_minutes: i32, // 5, 15, 60, 360
376376
}
377377

378378
#[derive(Debug, Serialize, Deserialize)]
379379
pub struct HistogramBucket {
380-
pub timestamp: String, // Bucket start time (RFC3339)
381-
pub count: i32, // Count in this bucket
380+
pub timestamp: String, // Bucket start time (RFC3339)
381+
pub count: i32, // Count in this bucket
382382
}
383383

384384
#[derive(Debug, Serialize, Deserialize)]

src-tauri/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ use commands::{
99
analyze_session, cancel_analysis, create_analysis, get_analysis_result,
1010
get_analysis_status, list_analyses, run_analysis,
1111
},
12-
file::{clear_opened_files, get_opened_files, handle_file_drop, import_sessions},
12+
file::{
13+
clear_opened_files, get_opened_files, handle_file_drop, import_from_provider,
14+
import_sessions,
15+
},
1316
histogram::{get_session_activity_histogram, get_user_message_histogram},
1417
session::{get_providers, get_session_detail, get_sessions, search_messages},
1518
};
@@ -170,6 +173,7 @@ pub async fn run() -> anyhow::Result<()> {
170173
get_opened_files,
171174
clear_opened_files,
172175
import_sessions,
176+
import_from_provider,
173177
get_session_activity_histogram,
174178
get_user_message_histogram,
175179
])

0 commit comments

Comments
 (0)