papayu/src/lib/tauri.ts
Yuriy 764003fc09 Commit X4: Auto inject Online Research summary into plan context
This commit implements fully automatic injection of online research results into the LLM prompt without user clicks.

## Backend

### Environment Variables
- Added `PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1` (default: 0) to enable automatic injection of online research results into subsequent `proposeActions` calls.
- Added `is_online_auto_use_as_context()` helper function in `online_research/mod.rs`.

### Command Changes
- **`propose_actions` command**: Added `online_fallback_reason: Option<String>` parameter to track the error code that triggered online fallback.
- **`llm_planner::plan` function**: Added `online_fallback_reason: Option<&str>` parameter for tracing.
- **Trace Enhancements**: Added `online_fallback_reason` field to trace when `online_fallback_executed` is true.

### Module Exports
- Made `extract_error_code_prefix` public in `online_research/fallback.rs` for frontend use.

## Frontend

### Project Settings
- Added `onlineAutoUseAsContext` state (persisted in `localStorage` as `papa_yu_online_auto_use_as_context`).
- Initialized from localStorage or defaults to `false`.
- Auto-saved to localStorage on change.

### Auto-Chain Flow
- When `plan.ok === false` and `plan.online_fallback_suggested` is present:
  - If `onlineAutoUseAsContext === true` and not already attempted for this goal (cycle protection via `lastGoalWithOnlineFallbackRef`):
    - Automatically calls `researchAnswer(query)`.
    - Truncates result to `8000` chars and `10` sources (frontend-side limits).
    - Immediately calls `proposeActions` again with:
      - `online_context_md`
      - `online_context_sources`
      - `online_fallback_executed: true`
      - `online_fallback_reason: error_code`
      - `online_fallback_attempted: true`
    - Displays the new plan/error without requiring "Use as context" button click.
  - If `onlineAutoUseAsContext === false` or already attempted:
    - Falls back to manual mode (shows online research block with "Use as context (once)" button).

### Cycle Protection
- `lastGoalWithOnlineFallbackRef` tracks the last goal that triggered online fallback.
- If the same goal triggers fallback again, auto-chain is skipped to prevent infinite loops.
- Maximum 1 auto-chain per user query.

### UI Enhancements
- **Online Research Block**:
  - When `onlineAutoUseAsContext === true`: displays "Auto-used ✓" badge.
  - Hides "Use as context (once)" button when auto-use is enabled.
  - Adds "Disable auto-use" button (red) to disable auto-use for the current project.
  - When disabled, shows system message: "Auto-use отключён для текущего проекта."

### API Updates
- **`proposeActions` in `tauri.ts`**: Added `onlineFallbackReason?: string | null` parameter.

## Tests

- **`online_context_auto_test.rs`**: Added unit tests for:
  - `test_is_online_auto_use_disabled_by_default`
  - `test_is_online_auto_use_enabled_when_set`
  - `test_extract_error_code_prefix_timeout`
  - `test_extract_error_code_prefix_schema`
  - `test_extract_error_code_prefix_empty_when_no_prefix`

All tests pass.

## Documentation

### README.md
- Added "Auto-use (X4)" subsection under "Online Research":
  - Describes `PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1` env var (default: 0).
  - Explains cycle protection: maximum 1 auto-chain per goal.
  - Documents UI behavior: "Auto-used ✓" badge and "Disable auto-use" button.

## Behavior Summary

**Without auto-use (default):**
1. `proposeActions` → error + `online_fallback_suggested`
2. UI calls `researchAnswer`
3. UI displays online research block with "Use as context (once)" button
4. User clicks button → sets `onlineContextPending` → next `proposeActions` includes context

**With auto-use enabled (`PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1`):**
1. `proposeActions` → error + `online_fallback_suggested`
2. UI calls `researchAnswer` automatically
3. UI displays online research block with "Auto-used ✓" badge
4. UI immediately calls `proposeActions` again with online context → displays new plan
5. If still fails → no retry (cycle protection)

## Build Status

-  Backend: `cargo build --lib` (2 warnings about unused code for future features)
-  Frontend: `npm run build`
-  Tests: `cargo test online_context_auto_test --lib` (5 passed)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 14:39:40 +03:00

268 lines
7.9 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
import type {
Action,
AgenticRunRequest,
AgenticRunResult,
AnalyzeReport,
ApplyTxResult,
BatchEvent,
GenerateActionsResult,
PreviewResult,
ProjectProfile,
Session,
TrendsResult,
UndoStatus,
VerifyResult,
} from "./types";
export interface UndoRedoState {
undo_available: boolean;
redo_available: boolean;
}
export interface RunBatchPayload {
paths: string[];
confirm_apply: boolean;
auto_check: boolean;
selected_actions?: Action[];
user_confirmed?: boolean;
attached_files?: string[];
}
export interface ApplyActionsTxOptions {
auto_check: boolean;
user_confirmed: boolean;
protocol_version_override?: number | null;
fallback_attempted?: boolean;
}
export interface ProjectItem {
id: string;
path: string;
}
export interface AddProjectResult {
id: string;
}
export interface UndoLastResult {
ok: boolean;
error_code?: string;
error?: string;
}
export async function getUndoRedoState(): Promise<UndoRedoState> {
return invoke<UndoRedoState>("get_undo_redo_state_cmd");
}
export async function getUndoStatus(): Promise<UndoStatus> {
return invoke<UndoStatus>("undo_status").catch(() => ({ available: false } as UndoStatus));
}
export async function getFolderLinks(): Promise<{ paths: string[] }> {
return invoke<{ paths: string[] }>("get_folder_links");
}
export async function setFolderLinks(paths: string[]): Promise<void> {
return invoke("set_folder_links", { links: { paths } });
}
export async function getProjectProfile(path: string): Promise<ProjectProfile> {
return invoke<ProjectProfile>("get_project_profile", { path });
}
export async function runBatchCmd(payload: RunBatchPayload): Promise<BatchEvent[]> {
return invoke<BatchEvent[]>("run_batch_cmd", { payload });
}
/** Предпросмотр diff для actions (CREATE/UPDATE/DELETE) без записи на диск. */
export async function previewActions(rootPath: string, actions: Action[]): Promise<PreviewResult> {
return invoke<PreviewResult>("preview_actions_cmd", {
payload: {
root_path: rootPath,
actions,
auto_check: null,
label: null,
user_confirmed: false,
},
});
}
export async function applyActionsTx(
path: string,
actions: Action[],
options: ApplyActionsTxOptions | boolean
): Promise<ApplyTxResult> {
const opts: ApplyActionsTxOptions =
typeof options === "boolean"
? { auto_check: options, user_confirmed: true }
: options;
return invoke<ApplyTxResult>("apply_actions_tx", {
path,
actions,
options: opts,
});
}
export async function generateActionsFromReport(
path: string,
report: AnalyzeReport,
mode: string
): Promise<GenerateActionsResult> {
return invoke<GenerateActionsResult>("generate_actions_from_report", {
path,
report,
mode,
});
}
export async function agenticRun(payload: AgenticRunRequest): Promise<AgenticRunResult> {
return invoke<AgenticRunResult>("agentic_run", { payload });
}
export async function listProjects(): Promise<ProjectItem[]> {
return invoke<ProjectItem[]>("list_projects");
}
export async function addProject(path: string, name: string | null): Promise<AddProjectResult> {
return invoke<AddProjectResult>("add_project", { path, name });
}
export async function listSessions(projectId?: string): Promise<Session[]> {
return invoke<Session[]>("list_sessions", { projectId: projectId ?? null });
}
export async function appendSessionEvent(
projectId: string,
kind: string,
role: string,
text: string
): Promise<void> {
return invoke("append_session_event", {
project_id: projectId,
kind,
role,
text,
});
}
export interface AgentPlanResult {
ok: boolean;
summary: string;
actions: Action[];
error?: string;
error_code?: string;
plan_json?: string;
plan_context?: string;
protocol_version_used?: number | null;
online_fallback_suggested?: string | null;
online_context_used?: boolean | null;
}
export async function proposeActions(
path: string,
reportJson: string,
userGoal: string,
designStyle?: string | null,
trendsContext?: string | null,
lastPlanJson?: string | null,
lastContext?: string | null,
applyErrorCode?: string | null,
applyErrorValidatedJson?: string | null,
applyRepairAttempt?: number | null,
applyErrorStage?: string | null,
onlineFallbackAttempted?: boolean | null,
onlineContextMd?: string | null,
onlineContextSources?: string[] | null,
onlineFallbackExecuted?: boolean | null,
onlineFallbackReason?: string | null
): Promise<AgentPlanResult> {
return invoke<AgentPlanResult>("propose_actions", {
path,
reportJson,
userGoal,
designStyle: designStyle ?? null,
trendsContext: trendsContext ?? null,
lastPlanJson: lastPlanJson ?? null,
lastContext: lastContext ?? null,
applyErrorCode: applyErrorCode ?? null,
applyErrorValidatedJson: applyErrorValidatedJson ?? null,
applyRepairAttempt: applyRepairAttempt ?? null,
applyErrorStage: applyErrorStage ?? null,
onlineFallbackAttempted: onlineFallbackAttempted ?? null,
onlineContextMd: onlineContextMd ?? null,
onlineContextSources: onlineContextSources ?? null,
onlineFallbackExecuted: onlineFallbackExecuted ?? null,
onlineFallbackReason: onlineFallbackReason ?? null,
});
}
export async function undoLastTx(path: string): Promise<boolean> {
return invoke<boolean>("undo_last_tx", { path });
}
export async function undoLast(): Promise<UndoLastResult> {
return invoke<UndoLastResult>("undo_last");
}
export async function redoLast(): Promise<UndoLastResult> {
return invoke<UndoLastResult>("redo_last");
}
/** Проверка целостности проекта (типы, сборка, тесты). Вызывается автоматически после применений или вручную. */
export async function verifyProject(path: string): Promise<VerifyResult> {
return invoke<VerifyResult>("verify_project", { path });
}
/** Тренды и рекомендации: последнее обновление и список. should_update === true если прошло >= 30 дней. */
export async function getTrendsRecommendations(): Promise<TrendsResult> {
return invoke<TrendsResult>("get_trends_recommendations");
}
/** Обновить тренды и рекомендации (запрос к внешним ресурсам по allowlist). */
export async function fetchTrendsRecommendations(): Promise<TrendsResult> {
return invoke<TrendsResult>("fetch_trends_recommendations");
}
// Settings export/import
export interface ImportResult {
projects_imported: number;
profiles_imported: number;
sessions_imported: number;
folder_links_imported: number;
}
/** Export all settings as JSON string */
export async function exportSettings(): Promise<string> {
return invoke<string>("export_settings");
}
/** Import settings from JSON string */
export async function importSettings(json: string, mode?: "replace" | "merge"): Promise<ImportResult> {
return invoke<ImportResult>("import_settings", { json, mode: mode ?? "merge" });
}
/** Еженедельный отчёт: агрегация трасс и генерация через LLM */
export async function analyzeWeeklyReports(
projectPath: string,
from?: string | null,
to?: string | null
): Promise<import("./types").WeeklyReportResult> {
return invoke("analyze_weekly_reports_cmd", {
projectPath,
from: from ?? null,
to: to ?? null,
});
}
/** Сохранить отчёт в docs/reports/weekly_YYYY-MM-DD.md */
export async function saveReport(projectPath: string, reportMd: string, date?: string | null): Promise<string> {
return invoke("save_report_cmd", { projectPath, reportMd, date: date ?? null });
}
/** Online research: поиск Tavily + fetch + LLM summarize. Требует PAPAYU_ONLINE_RESEARCH=1, PAPAYU_TAVILY_API_KEY. */
export async function researchAnswer(query: string): Promise<import("./types").OnlineAnswer> {
return invoke("research_answer_cmd", { query });
}