Skip to content
Merged
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
3 changes: 2 additions & 1 deletion apps/native/src-tauri/examples/specta_gen_ts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ fn main() {
.register::<shared_types::EvolutionResult>()
.register::<shared_types::EvolutionFailureResult>()
.register::<shared_types::RollbackResult>()
.register::<shared_types::SetDirResult>();
.register::<shared_types::SetDirResult>()
.register::<shared_types::UiPrefs>();

let shared_output_path = "../src/types/shared.ts";

Expand Down
24 changes: 21 additions & 3 deletions apps/native/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,12 +717,19 @@ pub async fn generate_history_from(

/// Summarizes the current working state, running the from-scratch pipeline if
/// no existing summaries are found, or grouping and simplifying existing ones.
/// Returns the updated SemanticChangeMap so the frontend can apply it immediately.
#[tauri::command]
pub async fn summarize_current(app: AppHandle) -> Result<(), String> {
pub async fn summarize_current(
app: AppHandle,
) -> Result<crate::shared_types::SemanticChangeMap, String> {
crate::summarize::new_changeset(&app, None)
.await
.map(|_| ())
.map_err(|e| capture_err("summarize_current", e))
.map_err(|e| capture_err("summarize_current", e))?;
let db_path = db::get_db_path(&app).map_err(|e| capture_err("summarize_current", e))?;
let dir = store::get_config_dir(&app).map_err(|e| capture_err("summarize_current", e))?;
let change_sets = crate::summarize::find_existing::for_current_state(&db_path, &dir)
.map_err(|e| capture_err("summarize_current", e))?;
Ok(crate::summarize::group_existing::from_change_sets(change_sets))
}

/// Returns all commits on the main branch, each paired with optional DB metadata, summary,
Expand Down Expand Up @@ -809,6 +816,9 @@ pub async fn ui_get_prefs(app: AppHandle) -> Result<types::UiPrefs, String> {
.map_err(|e| capture_err("ui_get_prefs", e))?;
let confirm_rollback = store::get_bool_pref(&app, store::CONFIRM_ROLLBACK_KEY, true)
.map_err(|e| capture_err("ui_get_prefs", e))?;
let auto_summarize_on_focus =
store::get_bool_pref(&app, store::AUTO_SUMMARIZE_ON_FOCUS_KEY, false)
.map_err(|e| capture_err("ui_get_prefs", e))?;

Ok(types::UiPrefs {
openrouter_api_key,
Expand All @@ -830,6 +840,7 @@ pub async fn ui_get_prefs(app: AppHandle) -> Result<types::UiPrefs, String> {
confirm_build,
confirm_clear,
confirm_rollback,
auto_summarize_on_focus,
})
}

Expand Down Expand Up @@ -906,6 +917,13 @@ pub async fn ui_set_prefs(
store::set_bool_pref(&app, store::CONFIRM_ROLLBACK_KEY, confirm_rollback)
.map_err(|e| capture_err("ui_set_prefs", e))?;
}
if let Some(auto_summarize_on_focus) = prefs
.get(store::AUTO_SUMMARIZE_ON_FOCUS_KEY)
.and_then(|v| v.as_bool())
{
store::set_bool_pref(&app, store::AUTO_SUMMARIZE_ON_FOCUS_KEY, auto_summarize_on_focus)
.map_err(|e| capture_err("ui_set_prefs", e))?;
}

Ok(serde_json::json!({"ok": true}))
}
Expand Down
26 changes: 26 additions & 0 deletions apps/native/src-tauri/src/shared_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,29 @@ pub struct EvolutionFailureResult {
pub git_status: Option<GitStatus>,
pub telemetry: EvolutionTelemetry,
}

// =============================================================================
// UI Preferences
// =============================================================================

/// User interface preferences (synced to settings.json via tauri-plugin-store).
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct UiPrefs {
pub openrouter_api_key: Option<String>,
pub openai_api_key: Option<String>,
pub ollama_api_base_url: Option<String>,
pub vllm_api_base_url: Option<String>,
pub vllm_api_key: Option<String>,
pub summary_provider: Option<String>,
pub summary_model: Option<String>,
pub evolve_provider: Option<String>,
pub evolve_model: Option<String>,
pub max_iterations: Option<usize>,
pub max_build_attempts: Option<usize>,
pub send_diagnostics: bool,
pub confirm_build: bool,
pub confirm_clear: bool,
pub confirm_rollback: bool,
pub auto_summarize_on_focus: bool,
}
9 changes: 8 additions & 1 deletion apps/native/src-tauri/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ use tauri_plugin_store::{Store, StoreExt};

const STORE_PATH: &str = "settings.json";

// Confirmation dialog preference keys (shared between store and commands)
// =============================================================================
// Preference Tab Keys
// =============================================================================

// Confirmation dialog preference keys
pub const CONFIRM_BUILD_KEY: &str = "confirmBuild";
pub const CONFIRM_CLEAR_KEY: &str = "confirmClear";
pub const CONFIRM_ROLLBACK_KEY: &str = "confirmRollback";

// Summarization preference keys
pub const AUTO_SUMMARIZE_ON_FOCUS_KEY: &str = "autoSummarizeOnFocus";

pub const DEFAULT_MAX_ITERATIONS: usize = 25;

/// Gets a handle to the settings store.
Expand Down
66 changes: 1 addition & 65 deletions apps/native/src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,71 +21,7 @@ pub struct Config {
pub host_attr: Option<String>,
}

pub use crate::shared_types::{GitFileStatus, GitStatus};

/// User interface preferences.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiPrefs {
/// OpenRouter API key for AI features via OpenRouter.
#[serde(rename = "openrouterApiKey")]
pub openrouter_api_key: Option<String>,

/// OpenAI API key for direct OpenAI access.
#[serde(rename = "openaiApiKey")]
pub openai_api_key: Option<String>,

/// Ollama API base URL for local model access.
#[serde(rename = "ollamaApiBaseUrl")]
pub ollama_api_base_url: Option<String>,

/// vLLM API base URL (OpenAI-compatible endpoint).
#[serde(rename = "vllmApiBaseUrl")]
pub vllm_api_base_url: Option<String>,

/// vLLM API key (optional — defaults to "none" if not set).
#[serde(rename = "vllmApiKey")]
pub vllm_api_key: Option<String>,

/// Provider for summarization (openai/ollama/vllm).
#[serde(rename = "summaryProvider")]
pub summary_provider: Option<String>,

/// Model name for summarization.
#[serde(rename = "summaryModel")]
pub summary_model: Option<String>,

/// Provider for evolution (openai/ollama).
#[serde(rename = "evolveProvider")]
pub evolve_provider: Option<String>,

/// Model name for evolution.
#[serde(rename = "evolveModel")]
pub evolve_model: Option<String>,

/// Maximum iterations for evolution before giving up.
#[serde(rename = "maxIterations")]
pub max_iterations: Option<usize>,

/// Maximum build attempts for evolution before giving up.
#[serde(rename = "maxBuildAttempts")]
pub max_build_attempts: Option<usize>,

/// Whether to send diagnostics to the nixmac team.
#[serde(rename = "sendDiagnostics")]
pub send_diagnostics: bool,

/// Whether to show a confirmation dialog before building.
#[serde(rename = "confirmBuild")]
pub confirm_build: bool,

/// Whether to show a confirmation dialog before clearing/discarding.
#[serde(rename = "confirmClear")]
pub confirm_clear: bool,

/// Whether to show a confirmation dialog before rolling back.
#[serde(rename = "confirmRollback")]
pub confirm_rollback: bool,
}
pub use crate::shared_types::{GitFileStatus, GitStatus, UiPrefs};

/// Result of a darwin-rebuild operation.
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
18 changes: 8 additions & 10 deletions apps/native/src/components/widget/analyze-history-button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Dna, Square } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useWidgetStore } from "@/stores/widget-store";
import { AnalyzeButton } from "@/components/widget/summaries/analyze-button";
import { useHistory } from "@/hooks/use-history";

import { useWidgetStore } from "@/stores/widget-store";
import { Dna, Square } from "lucide-react";

// Generates commit (if need be) and summary metadata for history items that are missing it.
export function AnalyzeHistoryButton() {
const history = useWidgetStore((state) => state.history);
const analyzingSize = useWidgetStore((state) => state.analyzingHistoryForHashes.size);
const analyzingSize = useWidgetStore(
(state) => state.analyzingHistoryForHashes.size,
);
const { analyzeMany, stopAnalyzing } = useHistory();

const recentUnsummarizedHashes = history
Expand All @@ -33,17 +35,13 @@ export function AnalyzeHistoryButton() {
}
if (recentUnsummarizedHashes.length > 0) {
return (
<Button
type="button"
variant="ghost"
size="sm"
<AnalyzeButton
disabled={analyzingSize === 1}
className="h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300"
onClick={() => analyzeMany(recentUnsummarizedHashes)}
>
<Dna className="h-[10px] w-[10px]" />
Analyze recent ({recentUnsummarizedHashes.length})
</Button>
</AnalyzeButton>
);
}

Expand Down
28 changes: 15 additions & 13 deletions apps/native/src/components/widget/analyze-history-item-button.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import { useState } from "react";
import { Dna, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useWidgetStore } from "@/stores/widget-store";
import { AnalyzeButton } from "@/components/widget/summaries/analyze-button";
import { useHistory } from "@/hooks/use-history";
import { useWidgetStore } from "@/stores/widget-store";
import { Dna, Loader2 } from "lucide-react";
import { useState } from "react";
interface AnalyzeHistoryItemButtonProps {
hash: string;
isPartial?: boolean;
className?: string;
}

export function AnalyzeHistoryItemButton({ hash, isPartial, className }: AnalyzeHistoryItemButtonProps) {
export function AnalyzeHistoryItemButton({
hash,
isPartial,
className,
}: AnalyzeHistoryItemButtonProps) {
const [localAnalyzing, setLocalAnalyzing] = useState(false);
const queuedByMany = useWidgetStore((state) => state.analyzingHistoryForHashes.has(hash));
const queuedByMany = useWidgetStore((state) =>
state.analyzingHistoryForHashes.has(hash),
);
const isAnalyzing = localAnalyzing || queuedByMany;
const { analyzeOne } = useHistory();

return (
<Button
type="button"
variant="ghost"
size="sm"
<AnalyzeButton
disabled={isAnalyzing}
className={cn("h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300", className)}
className={className}
onClick={async (e) => {
e.stopPropagation();
setLocalAnalyzing(true);
Expand All @@ -44,6 +46,6 @@ export function AnalyzeHistoryItemButton({ hash, isPartial, className }: Analyze
{isPartial ? "Update" : "Analyze"}
</>
)}
</Button>
</AnalyzeButton>
);
}
18 changes: 18 additions & 0 deletions apps/native/src/components/widget/settings/preferences-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function PreferencesTab() {
const confirmBuild = useWidgetStore((s) => s.confirmBuild);
const confirmClear = useWidgetStore((s) => s.confirmClear);
const confirmRollback = useWidgetStore((s) => s.confirmRollback);
const autoSummarizeOnFocus = useWidgetStore((s) => s.autoSummarizeOnFocus);

return (
<div className="space-y-6">
Expand Down Expand Up @@ -48,6 +49,23 @@ export function PreferencesTab() {
</div>
</div>
</div>
<div>
<div className="space-y-3">
<div className="font-medium text-sm">Summarization</div>
<div className="space-y-2">
<div className="flex items-center justify-between rounded-lg border border-border p-3">
<div className="space-y-0.5">
<div className="text-sm">Auto-summarize on focus</div>
<div className="text-muted-foreground text-xs">Summarize unsummarized changes when the window is focused</div>
</div>
<Switch
checked={autoSummarizeOnFocus}
onCheckedChange={(checked) => setPref("autoSummarizeOnFocus", checked)}
/>
</div>
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Idle 1`] = `"<button class="inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg:not([class*='size-'])]:size-4 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 dark:hover:bg-accent/50 rounded-md has-[&gt;svg]:px-2.5 h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300" data-slot="button" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dna h-[10px] w-[10px]" aria-hidden="true"><path d="m10 16 1.5 1.5"></path><path d="m14 8-1.5-1.5"></path><path d="M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"></path><path d="m16.5 10.5 1 1"></path><path d="m17 6-2.891-2.891"></path><path d="M2 15c6.667-6 13.333 0 20-6"></path><path d="m20 9 .891.891"></path><path d="M3.109 14.109 4 15"></path><path d="m6.5 12.5 1 1"></path><path d="m7 18 2.891 2.891"></path><path d="M9 22c1.798-1.998 2.518-3.995 2.807-5.993"></path></svg>Analyze</button>"`;

exports[`Loading 1`] = `"<button class="inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg:not([class*='size-'])]:size-4 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 dark:hover:bg-accent/50 rounded-md has-[&gt;svg]:px-2.5 h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300" data-slot="button" type="button" disabled=""><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-loader-circle h-[10px] w-[10px] animate-spin" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"></path></svg>Analyzing…</button>"`;

exports[`Update 1`] = `"<button class="inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg:not([class*='size-'])]:size-4 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 dark:hover:bg-accent/50 rounded-md has-[&gt;svg]:px-2.5 h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300" data-slot="button" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dna h-[10px] w-[10px]" aria-hidden="true"><path d="m10 16 1.5 1.5"></path><path d="m14 8-1.5-1.5"></path><path d="M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"></path><path d="m16.5 10.5 1 1"></path><path d="m17 6-2.891-2.891"></path><path d="M2 15c6.667-6 13.333 0 20-6"></path><path d="m20 9 .891.891"></path><path d="M3.109 14.109 4 15"></path><path d="m6.5 12.5 1 1"></path><path d="m7 18 2.891 2.891"></path><path d="M9 22c1.798-1.998 2.518-3.995 2.807-5.993"></path></svg>Update</button>"`;

exports[`With Count 1`] = `"<button class="inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&amp;_svg:not([class*='size-'])]:size-4 [&amp;_svg]:pointer-events-none [&amp;_svg]:shrink-0 dark:hover:bg-accent/50 rounded-md has-[&gt;svg]:px-2.5 h-auto gap-[3px] px-[7px] py-0.5 text-[10px] text-neutral-500 hover:bg-transparent hover:text-neutral-300" data-slot="button" type="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dna h-[10px] w-[10px]" aria-hidden="true"><path d="m10 16 1.5 1.5"></path><path d="m14 8-1.5-1.5"></path><path d="M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"></path><path d="m16.5 10.5 1 1"></path><path d="m17 6-2.891-2.891"></path><path d="M2 15c6.667-6 13.333 0 20-6"></path><path d="m20 9 .891.891"></path><path d="M3.109 14.109 4 15"></path><path d="m6.5 12.5 1 1"></path><path d="m7 18 2.891 2.891"></path><path d="M9 22c1.798-1.998 2.518-3.995 2.807-5.993"></path></svg>Analyze recent (3)</button>"`;
Loading
Loading