Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 268 additions & 5 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decor
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.31", features = ["bundled"] }
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }

[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
tauri-plugin-process = "2.3.1"
Expand Down
171 changes: 170 additions & 1 deletion src-tauri/src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use std::sync::OnceLock;

use rusqlite::{params, Connection, OptionalExtension};

use crate::models::{PullRequestCore, PullRequestSummary, RepoSummary};
use crate::models::{
LlmSettings, PullRequestChapters, PullRequestCore, PullRequestSummary, RepoSummary,
};
use crate::support::{bool_to_sql, now_unix_timestamp, sql_to_bool};

static CACHE_DB_PATH: OnceLock<PathBuf> = OnceLock::new();
Expand Down Expand Up @@ -200,6 +202,27 @@ pub fn initialize_cache_database(path: &Path) -> Result<(), String> {
PRIMARY KEY (repo_name_with_owner, pr_number, head_sha)
);

CREATE TABLE IF NOT EXISTS pr_chapters_cache (
repo_name_with_owner TEXT NOT NULL,
pr_number INTEGER NOT NULL,
head_sha TEXT NOT NULL,
prompt_version TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
chapters_json TEXT NOT NULL,
cached_at INTEGER NOT NULL,
last_accessed_at INTEGER NOT NULL,
PRIMARY KEY (repo_name_with_owner, pr_number, head_sha, prompt_version)
);

CREATE TABLE IF NOT EXISTS llm_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
provider TEXT NOT NULL,
model TEXT NOT NULL,
base_url TEXT,
updated_at INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS tracked_pull_requests (
repo_name_with_owner TEXT NOT NULL,
pr_number INTEGER NOT NULL,
Expand Down Expand Up @@ -511,6 +534,152 @@ pub fn store_changed_files(
Ok(())
}

pub fn read_cached_pull_request_chapters(
repo: &str,
number: u32,
head_sha: &str,
prompt_version: &str,
) -> Result<Option<PullRequestChapters>, String> {
let conn = open_cache_connection()?;
let chapters_json = conn
.query_row(
"
SELECT chapters_json
FROM pr_chapters_cache
WHERE repo_name_with_owner = ?1
AND pr_number = ?2
AND head_sha = ?3
AND prompt_version = ?4
",
params![repo, number, head_sha, prompt_version],
|row| row.get::<_, String>(0),
)
.optional()
.map_err(|error| format!("Failed to query cached PR chapters: {error}"))?;

let Some(chapters_json) = chapters_json else {
return Ok(None);
};

conn.execute(
"
UPDATE pr_chapters_cache
SET last_accessed_at = ?5
WHERE repo_name_with_owner = ?1
AND pr_number = ?2
AND head_sha = ?3
AND prompt_version = ?4
",
params![repo, number, head_sha, prompt_version, now_unix_timestamp()],
)
.map_err(|error| format!("Failed to update PR chapters cache access time: {error}"))?;

let chapters = serde_json::from_str::<PullRequestChapters>(&chapters_json)
.map_err(|error| format!("Failed to parse cached PR chapters: {error}"))?;

Ok(Some(chapters))
}

pub fn store_pull_request_chapters(chapters: &PullRequestChapters) -> Result<(), String> {
let conn = open_cache_connection()?;
let chapters_json = serde_json::to_string(chapters)
.map_err(|error| format!("Failed to serialize PR chapters for cache: {error}"))?;
let timestamp = now_unix_timestamp();

conn.execute(
"
INSERT INTO pr_chapters_cache (
repo_name_with_owner,
pr_number,
head_sha,
prompt_version,
provider,
model,
chapters_json,
cached_at,
last_accessed_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8)
ON CONFLICT(repo_name_with_owner, pr_number, head_sha, prompt_version)
DO UPDATE SET
provider = excluded.provider,
model = excluded.model,
chapters_json = excluded.chapters_json,
cached_at = excluded.cached_at,
last_accessed_at = excluded.last_accessed_at
",
params![
&chapters.repo,
chapters.number,
&chapters.head_sha,
&chapters.prompt_version,
&chapters.provider,
&chapters.model,
chapters_json,
timestamp,
],
)
.map_err(|error| format!("Failed to persist PR chapters cache: {error}"))?;

Ok(())
}

pub fn read_llm_settings() -> Result<Option<LlmSettings>, String> {
let conn = open_cache_connection()?;
let settings = conn
.query_row(
"
SELECT provider, model, base_url
FROM llm_settings
WHERE id = 1
",
[],
|row| {
Ok(LlmSettings {
provider: row.get(0)?,
model: row.get(1)?,
base_url: row.get(2)?,
has_api_key: false,
})
},
)
.optional()
.map_err(|error| format!("Failed to read LLM settings: {error}"))?;

Ok(settings)
}

pub fn write_llm_settings(settings: &LlmSettings) -> Result<(), String> {
let conn = open_cache_connection()?;
conn.execute(
"
INSERT INTO llm_settings (
id,
provider,
model,
base_url,
updated_at
)
VALUES (1, ?1, ?2, ?3, ?4)
ON CONFLICT(id)
DO UPDATE SET
provider = excluded.provider,
model = excluded.model,
base_url = excluded.base_url,
updated_at = excluded.updated_at
",
params![
&settings.provider,
&settings.model,
&settings.base_url,
now_unix_timestamp(),
],
)
.map_err(|error| format!("Failed to persist LLM settings: {error}"))?;

Ok(())
}

pub fn update_repo_access_timestamp(repo: &str) -> Result<(), String> {
let conn = open_cache_connection()?;
conn.execute(
Expand Down
72 changes: 72 additions & 0 deletions src-tauri/src/commands/chapters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::models::{LlmProviderInfo, LlmSettings, PullRequestChapters, SaveLlmSettingsInput};
use crate::services::{chapters, llm};

async fn run_blocking_task<T, F>(task: F) -> Result<T, String>
where
T: Send + 'static,
F: FnOnce() -> Result<T, String> + Send + 'static,
{
tauri::async_runtime::spawn_blocking(task)
.await
.map_err(|error| format!("Blocking task failed: {error}"))?
}

#[tauri::command]
pub fn list_llm_providers() -> Vec<LlmProviderInfo> {
llm::list_provider_infos()
}

#[tauri::command]
pub async fn get_llm_settings() -> Result<LlmSettings, String> {
run_blocking_task(llm::load_llm_settings).await
}

#[tauri::command]
pub async fn save_llm_settings(settings: SaveLlmSettingsInput) -> Result<LlmSettings, String> {
run_blocking_task(move || llm::save_llm_settings(settings)).await
}

#[tauri::command]
pub async fn set_llm_api_key(provider: String, api_key: String) -> Result<LlmSettings, String> {
run_blocking_task(move || {
llm::save_api_key(&provider, &api_key)?;
llm::load_llm_settings()
})
.await
}

#[tauri::command]
pub async fn delete_llm_api_key(provider: String) -> Result<LlmSettings, String> {
run_blocking_task(move || {
llm::delete_api_key(&provider)?;
llm::load_llm_settings()
})
.await
}

#[tauri::command]
pub async fn test_llm_provider() -> Result<(), String> {
run_blocking_task(|| {
let settings = llm::load_llm_settings()?;
llm::test_llm_provider(&settings)
})
.await
}

#[tauri::command]
pub async fn get_pull_request_chapters(
repo: String,
number: u32,
head_sha: String,
) -> Result<Option<PullRequestChapters>, String> {
run_blocking_task(move || chapters::read_cached_chapters(repo, number, head_sha)).await
}

#[tauri::command]
pub async fn regenerate_pull_request_chapters(
repo: String,
number: u32,
head_sha: String,
) -> Result<PullRequestChapters, String> {
run_blocking_task(move || chapters::generate_chapters(repo, number, head_sha)).await
}
3 changes: 2 additions & 1 deletion src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod pull_requests;
pub mod chapters;
pub mod preflight;
pub mod pull_requests;
pub mod repos;
pub mod review_comments;
pub mod tracked_pull_requests;
8 changes: 8 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ pub fn run() {
commands::repos::validate_repo,
commands::repos::list_saved_repos,
commands::repos::save_repo,
commands::chapters::list_llm_providers,
commands::chapters::get_llm_settings,
commands::chapters::save_llm_settings,
commands::chapters::set_llm_api_key,
commands::chapters::delete_llm_api_key,
commands::chapters::test_llm_provider,
commands::chapters::get_pull_request_chapters,
commands::chapters::regenerate_pull_request_chapters,
commands::preflight::get_gh_cli_status,
commands::pull_requests::list_cached_pull_requests,
commands::pull_requests::list_pull_requests,
Expand Down
96 changes: 96 additions & 0 deletions src-tauri/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,102 @@ pub struct PrPatch {
pub patch: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LlmProviderInfo {
pub id: String,
pub name: String,
pub adapter: String,
pub default_model: String,
pub default_base_url: Option<String>,
pub base_url_required: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LlmSettings {
pub provider: String,
pub model: String,
pub base_url: Option<String>,
pub has_api_key: bool,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SaveLlmSettingsInput {
pub provider: String,
pub model: String,
pub base_url: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ChapterKeyChange {
pub title: String,
pub detail: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ChapterReviewFocus {
pub title: String,
pub detail: String,
pub path: Option<String>,
pub severity: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ChapterPrologue {
pub summary: String,
pub key_changes: Vec<ChapterKeyChange>,
pub review_focus: Vec<ChapterReviewFocus>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestChapterFile {
pub path: String,
pub reason: String,
pub additions: u32,
pub deletions: u32,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ChapterReviewStep {
pub title: String,
pub detail: String,
pub files: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestChapter {
pub id: String,
pub title: String,
pub summary: String,
pub files: Vec<PullRequestChapterFile>,
pub review_steps: Vec<ChapterReviewStep>,
pub risks: Vec<ChapterReviewFocus>,
pub additions: u32,
pub deletions: u32,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestChapters {
pub repo: String,
pub number: u32,
pub head_sha: String,
pub provider: String,
pub model: String,
pub prompt_version: String,
pub generated_at: i64,
pub prologue: ChapterPrologue,
pub chapters: Vec<PullRequestChapter>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewComment {
Expand Down
Loading