mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(frontend): show plan content in the planning tab (#11807)
This commit is contained in:
parent
e7e49c9110
commit
d9731b6850
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
17
frontend/src/hooks/mutation/use-read-conversation-file.ts
Normal file
17
frontend/src/hooks/mutation/use-read-conversation-file.ts
Normal 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),
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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 50–80 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",
|
||||
|
||||
@ -6,7 +6,8 @@ type EventType =
|
||||
| "Terminal"
|
||||
| "FileEditor"
|
||||
| "StrReplaceEditor"
|
||||
| "TaskTracker";
|
||||
| "TaskTracker"
|
||||
| "PlanningFileEditor";
|
||||
|
||||
type ActionOnlyType =
|
||||
| "BrowserNavigate"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user