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>
268 lines
7.9 KiB
TypeScript
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 });
|
|
}
|