Skip to content

Commit

Permalink
Merge branch 'main' into kevin
Browse files Browse the repository at this point in the history
  • Loading branch information
SmartManoj committed Dec 12, 2024
2 parents b6b3286 + e979f51 commit e3a0e34
Show file tree
Hide file tree
Showing 40 changed files with 587 additions and 170 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ghcr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: "Set up docker layer caching"
uses: satackey/[email protected]
continue-on-error: true
- name: Build and push app image
if: "!github.event.pull_request.head.repo.fork"
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/py-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --forked --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:
Expand Down
3 changes: 3 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ llm_config = 'gpt3'
# Use host network
#use_host_network = false

# runtime extra build args
#runtime_extra_build_args = ["--network=host", "--add-host=host.docker.internal:host-gateway"]

# Enable auto linting after editing
#enable_auto_lint = false

Expand Down
2 changes: 1 addition & 1 deletion docs/modules/usage/how-to/headless-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ docker run -it \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.15 \
python -m openhands.core.main -t "write a bash script that prints hi"
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
60 changes: 60 additions & 0 deletions frontend/__tests__/components/chat/expandable-message.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { ExpandableMessage } from "#/components/features/chat/expandable-message";

describe("ExpandableMessage", () => {
it("should render with neutral border for non-action messages", () => {
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
const element = screen.getByText("Hello");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});

it("should render with neutral border for error messages", () => {
renderWithProviders(<ExpandableMessage message="Error occurred" type="error" />);
const element = screen.getByText("Error occurred");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});

it("should render with success icon for successful action messages", () => {
renderWithProviders(
<ExpandableMessage
message="Command executed successfully"
type="action"
success={true}
/>
);
const element = screen.getByText("Command executed successfully");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-success");
});

it("should render with error icon for failed action messages", () => {
renderWithProviders(
<ExpandableMessage
message="Command failed"
type="action"
success={false}
/>
);
const element = screen.getByText("Command failed");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-danger");
});

it("should render with neutral border and no icon for action messages without success prop", () => {
renderWithProviders(<ExpandableMessage message="Running command" type="action" />);
const element = screen.getByText("Running command");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
});
58 changes: 57 additions & 1 deletion frontend/__tests__/components/interactive-chat-box.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, within } from "@testing-library/react";
import { render, screen, within, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
Expand Down Expand Up @@ -131,4 +131,60 @@ describe("InteractiveChatBox", () => {
await user.click(stopButton);
expect(onStopMock).toHaveBeenCalledOnce();
});

it("should handle image upload and message submission correctly", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const onStop = vi.fn();
const onChange = vi.fn();

const { rerender } = render(
<InteractiveChatBox
onSubmit={onSubmit}
onStop={onStop}
onChange={onChange}
value="test message"
/>
);

// Upload an image via the upload button - this should NOT clear the text input
const file = new File(["dummy content"], "test.png", { type: "image/png" });
const input = screen.getByTestId("upload-image-input");
await user.upload(input, file);

// Verify text input was not cleared
expect(screen.getByRole("textbox")).toHaveValue("test message");
expect(onChange).not.toHaveBeenCalledWith("");

// Submit the message with image
const submitButton = screen.getByRole("button", { name: "Send" });
await user.click(submitButton);

// Verify onSubmit was called with the message and image
expect(onSubmit).toHaveBeenCalledWith("test message", [file]);

// Verify onChange was called to clear the text input
expect(onChange).toHaveBeenCalledWith("");

// Simulate parent component updating the value prop
rerender(
<InteractiveChatBox
onSubmit={onSubmit}
onStop={onStop}
onChange={onChange}
value=""
/>
);

// Verify the text input was cleared
expect(screen.getByRole("textbox")).toHaveValue("");

// Upload another image - this should NOT clear the text input
onChange.mockClear();
await user.upload(input, file);

// Verify text input is still empty and onChange was not called
expect(screen.getByRole("textbox")).toHaveValue("");
expect(onChange).not.toHaveBeenCalled();
});
});
10 changes: 7 additions & 3 deletions frontend/src/components/features/chat/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ export function ChatInput({

const handleSubmitMessage = () => {
const trimmedValue = textareaRef.current?.value.trim();
if (trimmedValue) {
onSubmit(trimmedValue);
textareaRef.current!.value = "";
if (value || (trimmedValue && !value)) {
onSubmit(value || trimmedValue || "");
if (value) {
onChange?.("");
} else if (textareaRef.current) {
textareaRef.current.value = "";
}
}
};

Expand Down
35 changes: 23 additions & 12 deletions frontend/src/components/features/chat/expandable-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import ArrowUp from "#/icons/angle-up-solid.svg?react";
import ArrowDown from "#/icons/angle-down-solid.svg?react";
import CheckCircle from "#/icons/check-circle-solid.svg?react";
import XCircle from "#/icons/x-circle-solid.svg?react";

interface ExpandableMessageProps {
id?: string;
message: string;
type: string;
success?: boolean;
}

export function ExpandableMessage({
id,
message,
type,
success,
}: ExpandableMessageProps) {
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
Expand All @@ -31,22 +35,14 @@ export function ExpandableMessage({
}
}, [id, message, i18n.language]);

const border = type === "error" ? "border-danger" : "border-neutral-300";
const textColor = type === "error" ? "text-danger" : "text-neutral-300";
let arrowClasses = "h-4 w-4 ml-2 inline";
if (type === "error") {
arrowClasses += " fill-danger";
} else {
arrowClasses += " fill-neutral-300";
}
const arrowClasses = "h-4 w-4 ml-2 inline fill-neutral-300";
const statusIconClasses = "h-4 w-4 ml-2 inline";

return (
<div
className={`flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 ${border}`}
>
<div className="flex gap-2 items-center justify-between border-l-2 border-neutral-300 pl-2 my-2 py-2">
<div className="text-sm leading-4 flex flex-col gap-2 max-w-full">
{headline && (
<p className={`${textColor} font-bold`}>
<p className="text-neutral-300 font-bold">
{headline}
<button
type="button"
Expand Down Expand Up @@ -75,6 +71,21 @@ export function ExpandableMessage({
</Markdown>
)}
</div>
{type === "action" && success !== undefined && (
<div className="flex-shrink-0">
{success ? (
<CheckCircle
data-testid="status-icon"
className={`${statusIconClasses} fill-success`}
/>
) : (
<XCircle
data-testid="status-icon"
className={`${statusIconClasses} fill-danger`}
/>
)}
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export function InteractiveChatBox({
const handleSubmit = (message: string) => {
onSubmit(message, images);
setImages([]);
if (message) {
onChange?.("");
}
};

return (
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/features/chat/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function Messages({
type={message.type}
id={message.translationID}
message={message.content}
success={message.success}
/>
);
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/icons/check-circle-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/src/icons/x-circle-solid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Message = {
timestamp: string;
imageUrls?: string[];
type?: "thought" | "error" | "action";
success?: boolean;
pending?: boolean;
translationID?: string;
eventID?: number;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/routes/_oh.app/hooks/use-ws-status-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const useWSStatusChange = () => {

if (gitHubToken && selectedRepository) {
dispatch(clearSelectedRepository());
additionalInfo = `Repository ${selectedRepository} has been cloned to /workspace. Please check the /workspace for files.`;
} else if (importedProjectZip) {
// if there's an uploaded project zip, add it to the chat
additionalInfo =
Expand Down
27 changes: 25 additions & 2 deletions frontend/src/state/chat-slice.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import { OpenHandsObservation } from "#/types/core/observations";
import {
OpenHandsObservation,
CommandObservation,
IPythonObservation,
} from "#/types/core/observations";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";

type SliceState = { messages: Message[] };

const MAX_CONTENT_LENGTH = 1000;

const HANDLED_ACTIONS = ["run", "run_ipython", "write", "read", "browse"];
const HANDLED_ACTIONS: OpenHandsEventType[] = [
"run",
"run_ipython",
"write",
"read",
"browse",
];

function getRiskText(risk: ActionSecurityRisk) {
switch (risk) {
Expand Down Expand Up @@ -131,6 +142,18 @@ export const chatSlice = createSlice({
return;
}
causeMessage.translationID = translationID;
// Set success property based on observation type
if (observationID === "run") {
const commandObs = observation.payload as CommandObservation;
causeMessage.success = commandObs.extras.exit_code === 0;
} else if (observationID === "run_ipython") {
// For IPython, we consider it successful if there's no error message
const ipythonObs = observation.payload as IPythonObservation;
causeMessage.success = !ipythonObs.message
.toLowerCase()
.includes("error");
}

if (observationID === "run" || observationID === "run_ipython") {
let { content } = observation.payload;
if (content.length > MAX_CONTENT_LENGTH) {
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/types/core/observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
};
}

export interface WriteObservation extends OpenHandsObservationEvent<"write"> {
source: "agent";
extras: {
path: string;
content: string;
};
}

export interface ReadObservation extends OpenHandsObservationEvent<"read"> {
source: "agent";
extras: {
path: string;
};
}

export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
source: "user";
extras: {
Expand All @@ -65,4 +80,6 @@ export type OpenHandsObservation =
| IPythonObservation
| DelegateObservation
| BrowseObservation
| WriteObservation
| ReadObservation
| ErrorObservation;
1 change: 1 addition & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
'root-secondary': '#262626',
'hyperlink': '#007AFF',
'danger': '#EF3744',
'success': '#4CAF50',
},
},
},
Expand Down
Loading

0 comments on commit e3a0e34

Please sign in to comment.