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
70 changes: 66 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ ntfs = "0.4"
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Registry"] }
globset = "0.4"
trash = "5"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "system-proxy"] }
tokio = { version = "1", features = ["full"] }
once_cell = "1"
chrono = { version = "0.4", features = ["serde"] }
Expand Down
22 changes: 21 additions & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use std::sync::Mutex;
use std::time::{Duration, SystemTime};

use include_dir::{include_dir, Dir};
use pinkbin_advisor::{advise as advise_provider, AdvisorRequest, AdvisorResponse, Provider};
use pinkbin_advisor::{
advise as advise_provider, chat as chat_provider, AdvisorRequest, AdvisorResponse, Provider,
};
use pinkbin_executor::{execute, Plan, UndoEntry};
use pinkbin_scaffold::{
compile_all, detect_compiled, detect_for, expand_env, load_dir, parse_toml, CompiledScaffold,
Expand Down Expand Up @@ -851,6 +853,23 @@ async fn advise(
.map_err(|e| e.to_string())
}

#[tauri::command]
async fn advisor_chat(
state: State<'_, AppState>,
system: String,
user: String,
) -> Result<String, String> {
let provider = state
.advisor
.lock()
.unwrap()
.clone()
.ok_or_else(|| "advisor not configured — open Settings".to_string())?;
chat_provider(&provider, &system, &user)
.await
.map_err(|e| e.to_string())
}

#[tauri::command]
fn inspect_path(path: String, sample_count: usize) -> Vec<String> {
sample_paths(&path, sample_count)
Expand Down Expand Up @@ -1332,6 +1351,7 @@ pub fn run() {
execute_scope,
list_conda_envs,
advise,
advisor_chat,
inspect_path,
reveal_in_explorer,
execute_plan,
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Splitter } from './components/Splitter';
import { Logo } from './components/Logo';
import { ErrorBoundary } from './components/ErrorBoundary';
import { formatBytes } from './format';
import { loadSettings, isConfigured } from './advisorClient';
import { loadSettings, isConfigured, syncAdvisorToBackend } from './advisorClient';

function isDriveRoot(p: string): boolean {
// C: / C:\ / C:/ — anything beyond is a subfolder
Expand Down Expand Up @@ -73,7 +73,12 @@ export default function App() {
const s = loadSettings();
setAdvisorTag(isConfigured(s) ? { provider: s.provider } : null);
};
useEffect(() => { refreshAdvisorTag(); }, []);
useEffect(() => {
refreshAdvisorTag();
// Settings live in localStorage; after a restart, hydrate Rust state so
// backend advisor commands work before the user reopens Settings.
syncAdvisorToBackend().catch(() => {});
}, []);
const [leftWidth, setLeftWidth] = useState<number>(() => {
const v = Number(localStorage.getItem('pinkbin.leftWidth'));
return Number.isFinite(v) && v > MIN_LEFT ? v : DEFAULT_LEFT;
Expand Down
24 changes: 22 additions & 2 deletions apps/desktop/src/advisorClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Browser-side AI advisor client — talks to OpenAI / Anthropic / Ollama directly
// from the browser, so the preview mode can give real answers.
// AI advisor client. Browser preview talks to providers directly. In the
// packaged Tauri app, text-only chat goes through the backend so compatible
// OpenAI relays do not need browser CORS support.
//
// Settings persist to localStorage under "pinkbin.advisor".

import { invoke } from '@tauri-apps/api/core';
import type { AdvisorRequest, AdvisorResponse } from './types';
import { isTauri } from './env';

export type Provider = 'openai' | 'anthropic' | 'gemini' | 'ollama';

Expand Down Expand Up @@ -179,6 +182,16 @@ export function isConfigured(s: AdvisorSettings | null): s is AdvisorSettings {
return Boolean(s.apiKey && s.model);
}

export async function syncAdvisorToBackend(settings = loadSettings()): Promise<void> {
if (!isTauri || !isConfigured(settings)) return;
await invoke<void>('set_advisor', {
provider: settings.provider,
apiKey: settings.provider === 'ollama' ? null : settings.apiKey,
model: settings.model,
baseUrl: settings.baseUrl || null,
});
}

const CHAT_SYSTEM = `You are Pinkbin's AI advisor — a friendly assistant that helps users figure out what their disk folders are and whether to delete them. Use the metadata you are given (the user's question references a folder by its path, size, samples). Be concise (2-4 sentences), in the user's language. If you suggest deleting, say what to delete (the whole folder vs a sub-scope) and via what mechanism (回收站 / 手动整理 / 卸载应用). Never recommend rm -rf on system paths.`;

const OVERVIEW_SYSTEM = `You are Pinkbin's AI advisor. The user just finished scanning their disk. You receive a JSON summary of the largest folders. Write a friendly Chinese overview (~180-220 字) covering, in order, with empty lines between sections:
Expand Down Expand Up @@ -227,6 +240,13 @@ async function runChatRaw(system: string, user: string, images?: ChatImage[]): P
const fullUser = user;
const imgs = images ?? [];

if (isTauri && imgs.length === 0) {
// Backend requests bypass browser CORS preflight; keep image chat on the
// browser path because provider-specific vision payloads already work here.
await syncAdvisorToBackend(settings);
return invoke<string>('advisor_chat', { system, user: fullUser });
}

if (settings.provider === 'openai') {
const url = (settings.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
const userContent: unknown = imgs.length === 0
Expand Down
Loading