feat(frontend): show plan content in the planning tab (#11807)

This commit is contained in:
Hiep Le 2025-12-01 20:42:44 +07:00 committed by GitHub
parent e7e49c9110
commit d9731b6850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 171 additions and 90 deletions

View File

@ -296,6 +296,25 @@ class V1ConversationService {
const { data } = await openHands.get<{ runtime_id: string }>(url);
return data;
}
/**
* Read a file from a specific conversation's sandbox workspace
* @param conversationId The conversation ID
* @param filePath Path to the file to read within the sandbox workspace (defaults to /workspace/project/PLAN.md)
* @returns The content of the file or an empty string if the file doesn't exist
*/
static async readConversationFile(
conversationId: string,
filePath: string = "/workspace/project/PLAN.md",
): Promise<string> {
const params = new URLSearchParams();
params.append("file_path", filePath);
const { data } = await openHands.get<string>(
`/api/v1/app-conversations/${conversationId}/file?${params.toString()}`,
);
return data;
}
}
export default V1ConversationService;

View File

@ -8,7 +8,7 @@ export function h1({
React.HTMLAttributes<HTMLHeadingElement> &
ExtraProps) {
return (
<h1 className="text-[32px] text-white font-bold leading-8 mb-4 mt-6 first:mt-0">
<h1 className="text-2xl text-white font-bold leading-8 mb-4 mt-6 first:mt-0">
{children}
</h1>
);

View File

@ -26,6 +26,7 @@ import {
isExecuteBashActionEvent,
isExecuteBashObservationEvent,
isConversationErrorEvent,
isPlanningFileEditorObservationEvent,
} from "#/types/v1/type-guards";
import { ConversationStateUpdateEventStats } from "#/types/v1/core/events/conversation-state-event";
import { handleActionEventCacheInvalidation } from "#/utils/cache-utils";
@ -38,6 +39,7 @@ import EventService from "#/api/event-service/event-service.api";
import { useConversationStore } from "#/state/conversation-store";
import { isBudgetOrCreditError } from "#/utils/error-handler";
import { useTracking } from "#/hooks/use-tracking";
import { useReadConversationFile } from "#/hooks/mutation/use-read-conversation-file";
import useMetricsStore from "#/stores/metrics-store";
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -102,12 +104,22 @@ export function ConversationWebSocketProvider({
number | null
>(null);
const { conversationMode } = useConversationStore();
const { conversationMode, setPlanContent } = useConversationStore();
// Hook for reading conversation file
const { mutate: readConversationFile } = useReadConversationFile();
// Separate received event count tracking per connection
const receivedEventCountRefMain = useRef(0);
const receivedEventCountRefPlanning = useRef(0);
// Track the latest PlanningFileEditorObservation event during history replay
// We'll only call the API once after history loading completes
const latestPlanningFileEventRef = useRef<{
path: string;
conversationId: string;
} | null>(null);
// Helper function to update metrics from stats event
const updateMetricsFromStats = useCallback(
(event: ConversationStateUpdateEventStats) => {
@ -235,11 +247,40 @@ export function ConversationWebSocketProvider({
receivedEventCountRefPlanning,
]);
// Call API once after history loading completes if we tracked any PlanningFileEditorObservation events
useEffect(() => {
if (!isLoadingHistoryPlanning && latestPlanningFileEventRef.current) {
const { path, conversationId: currentPlanningConversationId } =
latestPlanningFileEventRef.current;
readConversationFile(
{
conversationId: currentPlanningConversationId,
filePath: path,
},
{
onSuccess: (fileContent) => {
setPlanContent(fileContent);
},
onError: (error) => {
// eslint-disable-next-line no-console
console.warn("Failed to read conversation file:", error);
},
},
);
// Clear the ref after calling the API
latestPlanningFileEventRef.current = null;
}
}, [isLoadingHistoryPlanning, readConversationFile, setPlanContent]);
useEffect(() => {
hasConnectedRefMain.current = false;
setIsLoadingHistoryPlanning(!!subConversationIds?.length);
setExpectedEventCountPlanning(null);
receivedEventCountRefPlanning.current = 0;
// Reset the tracked event ref when sub-conversations change
latestPlanningFileEventRef.current = null;
}, [subConversationIds]);
// Merged loading history state - true if either connection is still loading
@ -254,6 +295,8 @@ export function ConversationWebSocketProvider({
setIsLoadingHistoryMain(true);
setExpectedEventCountMain(null);
receivedEventCountRefMain.current = 0;
// Reset the tracked event ref when conversation changes
latestPlanningFileEventRef.current = null;
}, [conversationId]);
// Separate message handlers for each connection
@ -438,6 +481,41 @@ export function ConversationWebSocketProvider({
.join("\n");
appendOutput(textContent);
}
// Handle PlanningFileEditorObservation events - read and update plan content
if (isPlanningFileEditorObservationEvent(event)) {
const planningAgentConversation = subConversations?.[0];
const planningConversationId = planningAgentConversation?.id;
if (planningConversationId && event.observation.path) {
// During history replay, track the latest event but don't call API
// After history loading completes, we'll call the API once with the latest event
if (isLoadingHistoryPlanning) {
latestPlanningFileEventRef.current = {
path: event.observation.path,
conversationId: planningConversationId,
};
} else {
// History loading is complete - this is a new real-time event
// Call the API immediately for real-time updates
readConversationFile(
{
conversationId: planningConversationId,
filePath: event.observation.path,
},
{
onSuccess: (fileContent) => {
setPlanContent(fileContent);
},
onError: (error) => {
// eslint-disable-next-line no-console
console.warn("Failed to read conversation file:", error);
},
},
);
}
}
}
}
} catch (error) {
// eslint-disable-next-line no-console
@ -455,6 +533,8 @@ export function ConversationWebSocketProvider({
setExecutionStatus,
appendInput,
appendOutput,
readConversationFile,
setPlanContent,
updateMetricsFromStats,
],
);

View File

@ -0,0 +1,17 @@
import { useMutation } from "@tanstack/react-query";
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
interface UseReadConversationFileVariables {
conversationId: string;
filePath?: string;
}
export const useReadConversationFile = () =>
useMutation({
mutationKey: ["read-conversation-file"],
mutationFn: async ({
conversationId,
filePath,
}: UseReadConversationFileVariables): Promise<string> =>
V1ConversationService.readConversationFile(conversationId, filePath),
});

View File

@ -23,7 +23,7 @@ function PlannerTab() {
const { planContent, setConversationMode } = useConversationStore();
if (planContent) {
if (planContent !== null && planContent !== undefined) {
return (
<div className="flex flex-col w-full h-full p-4 overflow-auto">
<Markdown

View File

@ -56,6 +56,7 @@ interface ConversationActions {
setHasRightPanelToggled: (hasRightPanelToggled: boolean) => void;
setConversationMode: (conversationMode: ConversationMode) => void;
setSubConversationTaskId: (taskId: string | null) => void;
setPlanContent: (planContent: string | null) => void;
}
type ConversationStore = ConversationState & ConversationActions;
@ -81,91 +82,7 @@ export const useConversationStore = create<ConversationStore>()(
submittedMessage: null,
shouldHideSuggestions: false,
hasRightPanelToggled: true,
planContent: `
# Improve Developer Onboarding and Examples
## Overview
Based on the analysis of Browser-Use's current documentation and examples, this plan addresses gaps in developer onboarding by creating a progressive learning path, troubleshooting resources, and practical examples that address real-world scenarios (like the LM Studio/local LLM integration issues encountered).
## Current State Analysis
**Strengths:**
- Good quickstart documentation in \`docs/quickstart.mdx\`
- Extensive examples across multiple categories (60+ example files)
- Well-structured docs with multiple LLM provider examples
- Active community support via Discord
**Gaps Identified:**
- No progressive tutorial series that builds complexity gradually
- Limited troubleshooting documentation for common issues
- Sparse comments in example files explaining what's happening
- Local LLM setup (Ollama/LM Studio) not prominently featured
- No "first 10 minutes" success path
- Missing visual/conceptual architecture guides for beginners
- Error messages don't always point to solutions
## Proposed Improvements
### 1. Create Interactive Tutorial Series (\`examples/tutorials/\`)
**New folder structure:**
\`\`\`
examples/tutorials/
README.md # Tutorial overview and prerequisites
00_hello_world.py # Absolute minimal example
01_your_first_search.py # Basic search with detailed comments
02_understanding_actions.py # How actions work
03_data_extraction_basics.py # Extract data step-by-step
04_error_handling.py # Common errors and solutions
05_custom_tools_intro.py # First custom tool
06_local_llm_setup.py # Ollama/LM Studio complete guide
07_debugging_tips.py # Debugging strategies
\`\`\`
**Key Features:**
- Each file 5080 lines max
- Extensive inline comments explaining every concept
- Clear learning objectives at the top of each file
- "What you'll learn" and "Prerequisites" sections
- Common pitfalls highlighted
- Expected output shown in comments
### 2. Troubleshooting Guide (\`docs/troubleshooting.mdx\`)
**Sections:**
- Installation issues (Chromium, dependencies, virtual environments)
- LLM provider connection errors (API keys, timeouts, rate limits)
- Local LLM setup (Ollama vs LM Studio, model compatibility)
- Browser automation issues (element not found, timeout errors)
- Common error messages with solutions
- Performance optimization tips
- When to ask for help (Discord/GitHub)
**Format:**
**Error: "LLM call timed out after 60 seconds"**
**What it means:**
The model took too long to respond
**Common causes:**
1. Model is too slow for the task
2. LM Studio/Ollama not responding properly
3. Complex page overwhelming the model
**Solutions:**
- Use flash_mode for faster execution
- Try a faster model (Gemini Flash, GPT-4 Turbo Mini)
- Simplify the task
- Check model server logs`,
planContent: null,
conversationMode: "code",
subConversationTaskId: null,
@ -304,6 +221,7 @@ The model took too long to respond
shouldHideSuggestions: false,
conversationMode: "code",
subConversationTaskId: null,
planContent: null,
},
false,
"resetConversationState",
@ -317,6 +235,9 @@ The model took too long to respond
setSubConversationTaskId: (subConversationTaskId) =>
set({ subConversationTaskId }, false, "setSubConversationTaskId"),
setPlanContent: (planContent) =>
set({ planContent }, false, "setPlanContent"),
}),
{
name: "conversation-store",

View File

@ -6,7 +6,8 @@ type EventType =
| "Terminal"
| "FileEditor"
| "StrReplaceEditor"
| "TaskTracker";
| "TaskTracker"
| "PlanningFileEditor";
type ActionOnlyType =
| "BrowserNavigate"

View File

@ -190,6 +190,38 @@ export interface TaskTrackerObservation
task_list: TaskItem[];
}
export interface PlanningFileEditorObservation
extends ObservationBase<"PlanningFileEditorObservation"> {
/**
* Content returned from the tool as a list of TextContent/ImageContent objects.
*/
content: Array<TextContent | ImageContent>;
/**
* Whether the call resulted in an error.
*/
is_error: boolean;
/**
* The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.
*/
command: "view" | "create" | "str_replace" | "insert" | "undo_edit";
/**
* The file path that was edited.
*/
path: string | null;
/**
* Indicates if the file previously existed. If not, it was created.
*/
prev_exist: boolean;
/**
* The content of the file before the edit.
*/
old_content: string | null;
/**
* The content of the file after the edit.
*/
new_content: string | null;
}
export type Observation =
| MCPToolObservation
| FinishObservation
@ -199,4 +231,5 @@ export type Observation =
| TerminalObservation
| FileEditorObservation
| StrReplaceEditorObservation
| TaskTrackerObservation;
| TaskTrackerObservation
| PlanningFileEditorObservation;

View File

@ -5,6 +5,7 @@ import {
ExecuteBashAction,
TerminalAction,
ExecuteBashObservation,
PlanningFileEditorObservation,
TerminalObservation,
} from "./core";
import { AgentErrorEvent } from "./core/events/observation-event";
@ -116,6 +117,15 @@ export const isExecuteBashObservationEvent = (
(event.observation.kind === "ExecuteBashObservation" ||
event.observation.kind === "TerminalObservation");
/**
* Type guard function to check if an observation event is a PlanningFileEditorObservation
*/
export const isPlanningFileEditorObservationEvent = (
event: OpenHandsEvent,
): event is ObservationEvent<PlanningFileEditorObservation> =>
isObservationEvent(event) &&
event.observation.kind === "PlanningFileEditorObservation";
/**
* Type guard function to check if an event is a system prompt event
*/