mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
fix(frontend): add rendering support for GlobObservation and GrepObservation events (#13379)
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getObservationContent } from "#/components/v1/chat/event-content-helpers/get-observation-content";
|
||||
import { ObservationEvent } from "#/types/v1/core";
|
||||
import { BrowserObservation } from "#/types/v1/core/base/observation";
|
||||
import {
|
||||
BrowserObservation,
|
||||
GlobObservation,
|
||||
GrepObservation,
|
||||
} from "#/types/v1/core/base/observation";
|
||||
|
||||
describe("getObservationContent - BrowserObservation", () => {
|
||||
it("should return output content when available", () => {
|
||||
@@ -90,3 +94,212 @@ describe("getObservationContent - BrowserObservation", () => {
|
||||
expect(result).toBe("**Output:**\nPage loaded successfully");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getObservationContent - GlobObservation", () => {
|
||||
it("should display files found when glob matches files", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GlobObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "glob",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GlobObservation",
|
||||
content: [{ type: "text", text: "Found 2 files", cache_prompt: false }],
|
||||
is_error: false,
|
||||
files: ["/workspace/src/index.ts", "/workspace/src/app.ts"],
|
||||
pattern: "**/*.ts",
|
||||
search_path: "/workspace",
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Pattern:** `**/*.ts`");
|
||||
expect(result).toContain("**Search Path:** `/workspace`");
|
||||
expect(result).toContain("**Files Found (2):**");
|
||||
expect(result).toContain("- `/workspace/src/index.ts`");
|
||||
expect(result).toContain("- `/workspace/src/app.ts`");
|
||||
});
|
||||
|
||||
it("should display no files found message when glob matches nothing", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GlobObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "glob",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GlobObservation",
|
||||
content: [{ type: "text", text: "No files found", cache_prompt: false }],
|
||||
is_error: false,
|
||||
files: [],
|
||||
pattern: "**/*.xyz",
|
||||
search_path: "/workspace",
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Pattern:** `**/*.xyz`");
|
||||
expect(result).toContain("**Result:** No files found.");
|
||||
});
|
||||
|
||||
it("should display error when glob operation fails", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GlobObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "glob",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GlobObservation",
|
||||
content: [{ type: "text", text: "Permission denied", cache_prompt: false }],
|
||||
is_error: true,
|
||||
files: [],
|
||||
pattern: "**/*",
|
||||
search_path: "/restricted",
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Error:**");
|
||||
expect(result).toContain("Permission denied");
|
||||
});
|
||||
|
||||
it("should indicate truncation when results exceed limit", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GlobObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "glob",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GlobObservation",
|
||||
content: [{ type: "text", text: "Found files", cache_prompt: false }],
|
||||
is_error: false,
|
||||
files: ["/workspace/file1.ts"],
|
||||
pattern: "**/*.ts",
|
||||
search_path: "/workspace",
|
||||
truncated: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Files Found (1+, truncated):**");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getObservationContent - GrepObservation", () => {
|
||||
it("should display matches found when grep finds results", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GrepObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "grep",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GrepObservation",
|
||||
content: [{ type: "text", text: "Found 2 matches", cache_prompt: false }],
|
||||
is_error: false,
|
||||
matches: ["/workspace/src/api.ts", "/workspace/src/routes.ts"],
|
||||
pattern: "fetchData",
|
||||
search_path: "/workspace",
|
||||
include_pattern: "*.ts",
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Pattern:** `fetchData`");
|
||||
expect(result).toContain("**Search Path:** `/workspace`");
|
||||
expect(result).toContain("**Include:** `*.ts`");
|
||||
expect(result).toContain("**Matches (2):**");
|
||||
expect(result).toContain("- `/workspace/src/api.ts`");
|
||||
});
|
||||
|
||||
it("should display no matches found when grep finds nothing", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GrepObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "grep",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GrepObservation",
|
||||
content: [{ type: "text", text: "No matches", cache_prompt: false }],
|
||||
is_error: false,
|
||||
matches: [],
|
||||
pattern: "nonExistentFunction",
|
||||
search_path: "/workspace",
|
||||
include_pattern: null,
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Pattern:** `nonExistentFunction`");
|
||||
expect(result).toContain("**Result:** No matches found.");
|
||||
expect(result).not.toContain("**Include:**");
|
||||
});
|
||||
|
||||
it("should display error when grep operation fails", () => {
|
||||
// Arrange
|
||||
const mockEvent: ObservationEvent<GrepObservation> = {
|
||||
id: "test-id",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
source: "environment",
|
||||
tool_name: "grep",
|
||||
tool_call_id: "call-id",
|
||||
action_id: "action-id",
|
||||
observation: {
|
||||
kind: "GrepObservation",
|
||||
content: [{ type: "text", text: "Invalid regex pattern", cache_prompt: false }],
|
||||
is_error: true,
|
||||
matches: [],
|
||||
pattern: "[invalid",
|
||||
search_path: "/workspace",
|
||||
include_pattern: null,
|
||||
truncated: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = getObservationContent(mockEvent);
|
||||
|
||||
// Assert
|
||||
expect(result).toContain("**Error:**");
|
||||
expect(result).toContain("Invalid regex pattern");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,6 +162,22 @@ const getObservationEventTitle = (event: OpenHandsEvent): React.ReactNode => {
|
||||
case "ThinkObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$THINK";
|
||||
break;
|
||||
case "GlobObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$GLOB";
|
||||
observationValues = {
|
||||
pattern: event.observation.pattern
|
||||
? trimText(event.observation.pattern, 50)
|
||||
: "",
|
||||
};
|
||||
break;
|
||||
case "GrepObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$GREP";
|
||||
observationValues = {
|
||||
pattern: event.observation.pattern
|
||||
? trimText(event.observation.pattern, 50)
|
||||
: "",
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// For unknown observations, use the type name
|
||||
return observationType.replace("Observation", "").toUpperCase();
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
FileEditorObservation,
|
||||
StrReplaceEditorObservation,
|
||||
TaskTrackerObservation,
|
||||
GlobObservation,
|
||||
GrepObservation,
|
||||
} from "#/types/v1/core/base/observation";
|
||||
|
||||
// File Editor Observations
|
||||
@@ -221,6 +223,72 @@ const getFinishObservationContent = (
|
||||
return content;
|
||||
};
|
||||
|
||||
// Glob Observations
|
||||
const getGlobObservationContent = (
|
||||
event: ObservationEvent<GlobObservation>,
|
||||
): string => {
|
||||
const { observation } = event;
|
||||
|
||||
// Extract text content from the observation
|
||||
const textContent = observation.content
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
|
||||
let content = `**Pattern:** \`${observation.pattern}\`\n`;
|
||||
content += `**Search Path:** \`${observation.search_path}\`\n\n`;
|
||||
|
||||
if (observation.is_error) {
|
||||
content += `**Error:**\n${textContent}`;
|
||||
} else if (observation.files.length === 0) {
|
||||
content += "**Result:** No files found.";
|
||||
} else {
|
||||
content += `**Files Found (${observation.files.length}${observation.truncated ? "+, truncated" : ""}):**\n`;
|
||||
content += observation.files.map((f) => `- \`${f}\``).join("\n");
|
||||
}
|
||||
|
||||
if (content.length > MAX_CONTENT_LENGTH) {
|
||||
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
// Grep Observations
|
||||
const getGrepObservationContent = (
|
||||
event: ObservationEvent<GrepObservation>,
|
||||
): string => {
|
||||
const { observation } = event;
|
||||
|
||||
// Extract text content from the observation
|
||||
const textContent = observation.content
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
|
||||
let content = `**Pattern:** \`${observation.pattern}\`\n`;
|
||||
content += `**Search Path:** \`${observation.search_path}\`\n`;
|
||||
if (observation.include_pattern) {
|
||||
content += `**Include:** \`${observation.include_pattern}\`\n`;
|
||||
}
|
||||
content += "\n";
|
||||
|
||||
if (observation.is_error) {
|
||||
content += `**Error:**\n${textContent}`;
|
||||
} else if (observation.matches.length === 0) {
|
||||
content += "**Result:** No matches found.";
|
||||
} else {
|
||||
content += `**Matches (${observation.matches.length}${observation.truncated ? "+, truncated" : ""}):**\n`;
|
||||
content += observation.matches.map((f) => `- \`${f}\``).join("\n");
|
||||
}
|
||||
|
||||
if (content.length > MAX_CONTENT_LENGTH) {
|
||||
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const getObservationContent = (event: ObservationEvent): string => {
|
||||
const observationType = event.observation.kind;
|
||||
|
||||
@@ -264,6 +332,16 @@ export const getObservationContent = (event: ObservationEvent): string => {
|
||||
event as ObservationEvent<FinishObservation>,
|
||||
);
|
||||
|
||||
case "GlobObservation":
|
||||
return getGlobObservationContent(
|
||||
event as ObservationEvent<GlobObservation>,
|
||||
);
|
||||
|
||||
case "GrepObservation":
|
||||
return getGrepObservationContent(
|
||||
event as ObservationEvent<GrepObservation>,
|
||||
);
|
||||
|
||||
default:
|
||||
return getDefaultEventContent(event);
|
||||
}
|
||||
|
||||
@@ -536,6 +536,8 @@ export enum I18nKey {
|
||||
OBSERVATION_MESSAGE$MCP = "OBSERVATION_MESSAGE$MCP",
|
||||
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
|
||||
OBSERVATION_MESSAGE$THINK = "OBSERVATION_MESSAGE$THINK",
|
||||
OBSERVATION_MESSAGE$GLOB = "OBSERVATION_MESSAGE$GLOB",
|
||||
OBSERVATION_MESSAGE$GREP = "OBSERVATION_MESSAGE$GREP",
|
||||
OBSERVATION_MESSAGE$TASK_TRACKING_PLAN = "OBSERVATION_MESSAGE$TASK_TRACKING_PLAN",
|
||||
OBSERVATION_MESSAGE$TASK_TRACKING_VIEW = "OBSERVATION_MESSAGE$TASK_TRACKING_VIEW",
|
||||
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
|
||||
|
||||
@@ -8575,6 +8575,38 @@
|
||||
"de": "Gedanke",
|
||||
"uk": "Думка"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$GLOB": {
|
||||
"en": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"ja": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"zh-CN": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"zh-TW": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"ko-KR": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"no": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"it": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"pt": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"es": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"ar": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"fr": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"tr": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"de": "Glob: <cmd>{{pattern}}</cmd>",
|
||||
"uk": "Glob: <cmd>{{pattern}}</cmd>"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$GREP": {
|
||||
"en": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"ja": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"zh-CN": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"zh-TW": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"ko-KR": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"no": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"it": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"pt": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"es": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"ar": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"fr": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"tr": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"de": "Grep: <cmd>{{pattern}}</cmd>",
|
||||
"uk": "Grep: <cmd>{{pattern}}</cmd>"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$TASK_TRACKING_PLAN": {
|
||||
"en": "Agent updated the plan",
|
||||
"zh-CN": "代理更新了计划",
|
||||
|
||||
@@ -27,7 +27,9 @@ type ActionEventType = `${ActionOnlyType}Action` | `${EventType}Action`;
|
||||
type ObservationEventType =
|
||||
| `${ObservationOnlyType}Observation`
|
||||
| `${EventType}Observation`
|
||||
| "TerminalObservation";
|
||||
| "TerminalObservation"
|
||||
| "GlobObservation"
|
||||
| "GrepObservation";
|
||||
|
||||
export interface ActionBase<T extends ActionEventType = ActionEventType> {
|
||||
kind: T;
|
||||
|
||||
@@ -217,6 +217,64 @@ export interface PlanningFileEditorObservation extends ObservationBase<"Planning
|
||||
new_content: string | null;
|
||||
}
|
||||
|
||||
export interface GlobObservation extends ObservationBase<"GlobObservation"> {
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* List of matching file paths sorted by modification time.
|
||||
*/
|
||||
files: string[];
|
||||
/**
|
||||
* The glob pattern that was used.
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* The directory that was searched.
|
||||
*/
|
||||
search_path: string;
|
||||
/**
|
||||
* Whether results were truncated to 100 files.
|
||||
*/
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface GrepObservation extends ObservationBase<"GrepObservation"> {
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* List of file paths containing the pattern.
|
||||
*/
|
||||
matches: string[];
|
||||
/**
|
||||
* The regex pattern that was used.
|
||||
*/
|
||||
pattern: string;
|
||||
/**
|
||||
* The directory that was searched.
|
||||
*/
|
||||
search_path: string;
|
||||
/**
|
||||
* The file pattern filter that was used.
|
||||
*/
|
||||
include_pattern: string | null;
|
||||
/**
|
||||
* Whether results were truncated to 100 files.
|
||||
*/
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export type Observation =
|
||||
| MCPToolObservation
|
||||
| FinishObservation
|
||||
@@ -227,4 +285,6 @@ export type Observation =
|
||||
| FileEditorObservation
|
||||
| StrReplaceEditorObservation
|
||||
| TaskTrackerObservation
|
||||
| PlanningFileEditorObservation;
|
||||
| PlanningFileEditorObservation
|
||||
| GlobObservation
|
||||
| GrepObservation;
|
||||
|
||||
Reference in New Issue
Block a user