Compare commits

..

1 Commits

Author SHA1 Message Date
Chuck Butkus f0589829c3 Test fix for event loop 2025-04-02 21:32:21 -04:00
139 changed files with 778 additions and 4902 deletions
BIN
View File
Binary file not shown.
@@ -40,9 +40,6 @@ def get_config(
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = AppConfig(
run_as_openhands=False,
max_budget_per_task=4,
@@ -145,7 +145,7 @@ while IFS= read -r task_image; do
docker pull $task_image
# Build the Python command
COMMAND="poetry run python -m evaluation.benchmarks.the_agent_company.run_infer \
COMMAND="poetry run python run_infer.py \
--agent-llm-config \"$AGENT_LLM_CONFIG\" \
--env-llm-config \"$ENV_LLM_CONFIG\" \
--outputs-path \"$OUTPUTS_PATH\" \
@@ -57,6 +57,6 @@ describe("Browser", () => {
});
expect(screen.getByText("https://example.com")).toBeInTheDocument();
expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument();
expect(screen.getByAltText(/browser screenshot/i)).toBeInTheDocument();
});
});
@@ -1,40 +0,0 @@
import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("CopyToClipboardButton", () => {
test("should have localized aria-label", () => {
render(
<CopyToClipboardButton
isHidden={false}
isDisabled={false}
onClick={() => {}}
mode="copy"
/>
);
const button = screen.getByTestId("copy-to-clipboard");
expect(button).toHaveAttribute("aria-label", "BUTTON$COPY");
});
test("should have localized aria-label when copied", () => {
render(
<CopyToClipboardButton
isHidden={false}
isDisabled={false}
onClick={() => {}}
mode="copied"
/>
);
const button = screen.getByTestId("copy-to-clipboard");
expect(button).toHaveAttribute("aria-label", "BUTTON$COPIED");
});
});
@@ -19,20 +19,6 @@ vi.mock("#/context/auth-context", () => ({
useAuth: vi.fn(),
}));
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
};
return translations[key] || key;
},
}),
}));
describe("ActionSuggestions", () => {
// Setup mocks for each test
beforeEach(() => {
@@ -98,7 +84,7 @@ describe("ActionSuggestions", () => {
const pushBranchPrompt =
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
const createPRPrompt =
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes. If a pull request template exists in the repository, please follow it when creating the PR description.";
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.";
// Verify the prompts are different
expect(pushBranchPrompt).not.toEqual(createPRPrompt);
@@ -15,31 +15,6 @@ import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
import { clickOnEditButton } from "./utils";
// We'll use the actual i18next implementation but override the translation function
import { I18nextProvider } from "react-i18next";
import i18n from "i18next";
// Mock the t function to return our custom translations
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"CONVERSATION$CREATED": "Created",
"CONVERSATION$AGO": "ago",
"CONVERSATION$UPDATED": "Updated"
};
return translations[key] || key;
},
i18n: {
changeLanguage: () => new Promise(() => {}),
},
}),
};
});
describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
@@ -72,18 +47,12 @@ describe("ConversationCard", () => {
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
const card = screen.getByTestId("conversation-card");
within(card).getByText("Conversation 1");
// Just check that the card contains the expected text content
expect(card).toHaveTextContent("Created");
expect(card).toHaveTextContent("ago");
// Use a regex to match the time part since it might have whitespace
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
expect(card).toHaveTextContent(timeRegex);
within(card).getByText(expectedDate);
});
it("should render the selectedRepository if available", () => {
@@ -372,7 +341,7 @@ describe("ConversationCard", () => {
await user.click(displayCostButton);
// Verify if metrics modal is displayed by checking for the modal content
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument();
expect(screen.getByText("Metrics Information")).toBeInTheDocument();
});
it("should not display the edit or delete options if the handler is not provided", async () => {
@@ -135,10 +135,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Cancel the deletion
const cancelButton = screen.getByRole("button", { name: /cancel/i });
const cancelButton = screen.getByText("Cancel");
await user.click(cancelButton);
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
// Ensure the conversation is not deleted
cards = await screen.findAllByTestId("conversation-card");
@@ -172,10 +172,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Confirm the deletion
const confirmButton = screen.getByRole("button", { name: /confirm/i });
const confirmButton = screen.getByText("Confirm");
await user.click(confirmButton);
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
// Wait for the cards to update with a longer timeout
await waitFor(() => {
@@ -239,10 +239,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Confirm the deletion
const confirmButton = screen.getByRole("button", { name: /confirm/i });
const confirmButton = screen.getByText("Confirm");
await user.click(confirmButton);
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
// Wait for the cards to update
await waitFor(() => {
@@ -63,7 +63,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
@@ -76,7 +76,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.125456");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
@@ -86,7 +86,7 @@ describe("PaymentForm", () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
expect(topUpButton).toBeDisabled();
const topUpInput = await screen.findByTestId("top-up-input");
@@ -102,7 +102,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(topUpButton).toBeDisabled();
@@ -116,7 +116,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "-50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -129,7 +129,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -142,7 +142,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "abc");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -155,7 +155,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "9"); // test assumes the minimum is 10
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -24,7 +24,7 @@ describe("WaitlistModal", () => {
const user = userEvent.setup();
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
const checkbox = screen.getByRole("checkbox");
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const button = screen.getByRole("button", { name: "Connect to GitHub" });
expect(button).toBeDisabled();
@@ -45,7 +45,7 @@ describe("WaitlistModal", () => {
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const button = screen.getByRole("button", { name: "Connect to GitHub" });
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
@@ -36,7 +36,7 @@ describe("UserAvatar", () => {
/>,
);
expect(screen.getByAltText("AVATAR$ALT_TEXT")).toBeInTheDocument();
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
expect(
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
@@ -63,6 +63,6 @@ describe("UserAvatar", () => {
/>,
);
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(screen.queryByAltText("AVATAR$ALT_TEXT")).not.toBeInTheDocument();
expect(screen.queryByAltText("user avatar")).not.toBeInTheDocument();
});
});
@@ -8,7 +8,6 @@ import {
WsClientProvider,
useWsClient,
} from "#/context/ws-client-provider";
import { AuthProvider } from "#/context/auth-context";
describe("Propagate error message", () => {
it("should do nothing when no message was passed from server", () => {
@@ -91,11 +90,9 @@ describe("WsClientProvider", () => {
const { getByText } = render(<TestComponent />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<AuthProvider initialProviderTokens={[]}>
<WsClientProvider conversationId="test-conversation-id">
{children}
</WsClientProvider>
</AuthProvider>
<WsClientProvider conversationId="test-conversation-id">
{children}
</WsClientProvider>
</QueryClientProvider>
),
});
@@ -88,6 +88,6 @@ describe("Settings Billing", () => {
await user.click(credits);
const billingSection = await screen.findByTestId("billing-settings");
within(billingSection).getByText("PAYMENT$MANAGE_CREDITS");
within(billingSection).getByText("Manage Credits");
});
});
+67 -25
View File
@@ -64,11 +64,11 @@ describe("Settings Screen", () => {
renderSettingsScreen();
await waitFor(() => {
// Use queryAllByText to handle multiple elements with the same text
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
screen.getByText("BUTTON$SAVE");
screen.getByText("LLM Settings");
screen.getByText("Git Provider Settings");
screen.getByText("Additional Settings");
screen.getByText("Reset to defaults");
screen.getByText("Save Changes");
});
});
@@ -150,7 +150,49 @@ describe("Settings Screen", () => {
}
});
// Tests for DISCONNECT_FROM_GITHUB button removed as the button is no longer included in main
it("should render a disabled 'Disconnect Tokens' button if the GitHub token is not set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect Tokens");
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});
it("should render an enabled 'Disconnect Tokens' button if any Git tokens are set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: mock_provider_tokens_are_set,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect Tokens");
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
// input should still be rendered
const input = await screen.findByTestId("github-token-input");
expect(input).toBeInTheDocument();
});
it("should logout the user when the 'Disconnect Tokens' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: mock_provider_tokens_are_set,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect Tokens");
await user.click(button);
expect(handleLogoutMock).toHaveBeenCalled();
});
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
@@ -165,7 +207,7 @@ describe("Settings Screen", () => {
renderSettingsScreen();
const button = screen.queryByText("GITHUB$CONFIGURE_REPOS");
const button = screen.queryByText("Configure GitHub Repositories");
expect(button).not.toBeInTheDocument();
});
@@ -182,7 +224,7 @@ describe("Settings Screen", () => {
});
renderSettingsScreen();
await screen.findByText("GITHUB$CONFIGURE_REPOS");
await screen.findByText("Configure GitHub Repositories");
});
it("should not render the GitHub token input if SaaS mode", async () => {
@@ -226,7 +268,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("github-token-input");
await user.type(input, "invalid-token");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
llmProviderInput = await screen.findByTestId("llm-provider-input");
@@ -506,7 +548,7 @@ describe("Settings Screen", () => {
const option = await screen.findByText("2x (4 core, 16G)");
await user.click(option);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -522,7 +564,7 @@ describe("Settings Screen", () => {
await toggleAdvancedSettings(user);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
await waitFor(() => {
@@ -553,7 +595,7 @@ describe("Settings Screen", () => {
await toggleAdvancedSettings(user);
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = screen.getByText("Reset to defaults");
await user.click(resetButton);
// show modal
@@ -601,7 +643,7 @@ describe("Settings Screen", () => {
);
await user.click(confirmationModeSwitch);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -714,7 +756,7 @@ describe("Settings Screen", () => {
expect(languageInput).toHaveValue("Norsk");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -751,7 +793,7 @@ describe("Settings Screen", () => {
const gpt4Option = await screen.findByText("gpt-4o");
await user.click(gpt4Option);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -776,7 +818,7 @@ describe("Settings Screen", () => {
expect(languageInput).toHaveValue("Norsk");
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = screen.getByText("Reset to defaults");
await user.click(resetButton);
expect(saveSettingsSpy).not.toHaveBeenCalled();
@@ -824,7 +866,7 @@ describe("Settings Screen", () => {
renderSettingsScreen();
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");
@@ -853,7 +895,7 @@ describe("Settings Screen", () => {
await user.click(analyticsConsentInput);
expect(analyticsConsentInput).toBeChecked();
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
@@ -867,7 +909,7 @@ describe("Settings Screen", () => {
);
renderSettingsScreen();
const saveButton = await screen.findByText("BUTTON$SAVE");
const saveButton = await screen.findByText("Save Changes");
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
@@ -900,7 +942,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -917,7 +959,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
expect(input).toHaveValue("");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -937,7 +979,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
expect(input).toHaveValue("");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -952,7 +994,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
await user.type(input, "new-api-key");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -1032,7 +1074,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const saveButton = await screen.findByText("BUTTON$SAVE");
const saveButton = await screen.findByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -1048,7 +1090,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");
@@ -2,8 +2,6 @@ import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
import { ChatInput } from "#/components/features/chat/chat-input";
import path from 'path';
import { scanDirectoryForUnlocalizedStrings } from "#/utils/scan-unlocalized-strings-ast";
// Mock react-i18next
vi.mock("react-i18next", () => ({
@@ -39,23 +37,4 @@ describe("Check for hardcoded English strings", () => {
render(<ChatInput onSubmit={() => {}} />);
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
});
test("No unlocalized strings should exist in frontend code", () => {
const srcPath = path.resolve(__dirname, '../../src');
// Get unlocalized strings using the AST scanner
// The scanner now properly handles CSS classes using AST information
const results = scanDirectoryForUnlocalizedStrings(srcPath);
// If we found any unlocalized strings, format them for output
if (results.size > 0) {
const formattedResults = Array.from(results.entries())
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
.join('\n');
throw new Error(
`Found unlocalized strings in the following files:${formattedResults}`
);
}
});
});
});
-3
View File
@@ -51,9 +51,6 @@
"ws": "^8.18.1"
},
"devDependencies": {
"@babel/parser": "^7.27.0",
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.51.1",
"@react-router/dev": "^7.4.0",
-3
View File
@@ -79,9 +79,6 @@
]
},
"devDependencies": {
"@babel/parser": "^7.27.0",
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.51.1",
"@react-router/dev": "^7.4.0",
+3 -1
View File
@@ -54,12 +54,14 @@ export const retrieveGitHubAppRepositories = async (
* Given a PAT, retrieves the repositories of the user
* @returns A list of repositories
*/
export const retrieveUserGitRepositories = async () => {
export const retrieveUserGitRepositories = async (page = 1, per_page = 30) => {
const response = await openHands.get<GitRepository[]>(
"/api/user/repositories",
{
params: {
sort: "pushed",
page,
per_page,
},
},
);
+6 -1
View File
@@ -224,7 +224,7 @@ class OpenHands {
}
static async createConversation(
selectedRepository?: GitRepository,
selectedRepository?: string,
initialUserMsg?: string,
imageUrls?: string[],
replayJson?: string,
@@ -323,6 +323,11 @@ class OpenHands {
return user;
}
static async getGitHubUserInstallationIds(): Promise<number[]> {
const response = await openHands.get<number[]>("/api/user/installations");
return response.data;
}
static async searchGitRepositories(
query: string,
per_page = 5,
@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import {
BaseModalTitle,
BaseModalDescription,
@@ -8,7 +7,6 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { BrandButton } from "../settings/brand-button";
import { I18nKey } from "#/i18n/declaration";
interface AnalyticsConsentFormModalProps {
onClose: () => void;
@@ -17,7 +15,6 @@ interface AnalyticsConsentFormModalProps {
export function AnalyticsConsentFormModal({
onClose,
}: AnalyticsConsentFormModalProps) {
const { t } = useTranslation();
const { mutate: saveUserSettings } = useSaveSettings();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -44,14 +41,16 @@ export function AnalyticsConsentFormModal({
className="flex flex-col gap-2"
>
<ModalBody className="border border-tertiary">
<BaseModalTitle title={t(I18nKey.ANALYTICS$TITLE)} />
<BaseModalTitle title="Your Privacy Preferences" />
<BaseModalDescription>
{t(I18nKey.ANALYTICS$DESCRIPTION)}
We use tools to understand how our application is used to improve
your experience. You can enable or disable analytics. Your
preferences will be stored and can be updated anytime.
</BaseModalDescription>
<label className="flex gap-2 items-center self-start">
<input name="analytics" type="checkbox" defaultChecked />
{t(I18nKey.ANALYTICS$SEND_ANONYMOUS_DATA)}
Send anonymous usage data
</label>
<BrandButton
@@ -60,7 +59,7 @@ export function AnalyticsConsentFormModal({
variant="primary"
className="w-full"
>
{t(I18nKey.ANALYTICS$CONFIRM_PREFERENCES)}
Confirm Preferences
</BrandButton>
</ModalBody>
</form>
@@ -1,19 +1,14 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface BrowserSnaphsotProps {
src: string;
}
export function BrowserSnapshot({ src }: BrowserSnaphsotProps) {
const { t } = useTranslation();
return (
<img
src={src}
style={{ objectFit: "contain", width: "100%", height: "auto" }}
className="rounded-xl"
alt={t(I18nKey.BROWSER$SCREENSHOT_ALT)}
alt="Browser Screenshot"
/>
);
}
@@ -1,11 +1,9 @@
import posthog from "posthog-js";
import React from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import type { RootState } from "#/store";
import { useAuth } from "#/context/auth-context";
import { I18nKey } from "#/i18n/declaration";
interface ActionSuggestionsProps {
onSuggestionsClick: (value: string) => void;
@@ -14,7 +12,6 @@ interface ActionSuggestionsProps {
export function ActionSuggestions({
onSuggestionsClick,
}: ActionSuggestionsProps) {
const { t } = useTranslation();
const { providersAreSet } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
@@ -38,7 +35,7 @@ export function ActionSuggestions({
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
createPR: `Please push the changes to ${
isGitLab ? "GitLab" : "GitHub"
} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
} and open a ${pr}. Please create a meaningful branch name that describes the changes.`,
pushToPR: `Please push the latest changes to the existing ${pr}.`,
};
@@ -50,7 +47,7 @@ export function ActionSuggestions({
<>
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_TO_BRANCH),
label: "Push to Branch",
value: terms.pushToBranch,
}}
onClick={(value) => {
@@ -60,7 +57,7 @@ export function ActionSuggestions({
/>
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_CREATE_PR),
label: `Push & Create ${terms.prShort}`,
value: terms.createPR,
}}
onClick={(value) => {
@@ -73,7 +70,7 @@ export function ActionSuggestions({
) : (
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_CHANGES_TO_PR),
label: `Push changes to ${terms.prShort}`,
value: terms.pushToPR,
}}
onClick={(value) => {
@@ -2,8 +2,6 @@ import { useDispatch, useSelector } from "react-redux";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { TrajectoryActions } from "../trajectory/trajectory-actions";
import { createChatMessage } from "#/services/chat-service";
@@ -38,7 +36,6 @@ function getEntryPoint(
export function ChatInterface() {
const { send, isLoadingMessages } = useWsClient();
const dispatch = useDispatch();
const { t } = useTranslation();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
useScrollToBottom(scrollRef);
@@ -97,19 +94,19 @@ export function ChatInterface() {
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
displayErrorToast("ConversationId unknown, cannot download trajectory");
return;
}
getTrajectory(params.conversationId, {
onSuccess: async (data) => {
await downloadTrajectory(
params.conversationId ?? t(I18nKey.CONVERSATION$UNKNOWN),
params.conversationId ?? "unknown",
data.trajectory,
);
},
onError: () => {
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
onError: (error) => {
displayErrorToast(error.message);
},
});
};
@@ -127,7 +124,7 @@ export function ChatInterface() {
<div
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2 fast-smooth-scroll"
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
>
{isLoadingMessages && (
<div className="flex justify-center">
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Link } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import ArrowUp from "#/icons/angle-up-solid.svg?react";
@@ -45,7 +44,7 @@ export function ExpandableMessage({
if (
config?.FEATURE_FLAGS.ENABLE_BILLING &&
config?.APP_MODE === "saas" &&
id === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS
id === "STATUS$ERROR_LLM_OUT_OF_CREDITS"
) {
return (
<div
@@ -54,13 +53,13 @@ export function ExpandableMessage({
>
<div className="text-sm w-full">
<div className="font-bold text-danger">
{t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)}
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")}
</div>
<Link
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
to="/settings/billing"
>
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
{t("BILLING$CLICK_TO_TOP_UP")}
</Link>
</div>
</div>
@@ -1,7 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { showErrorToast } from "#/utils/error-handler";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
@@ -79,7 +78,7 @@ export function AgentStatusBar() {
React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING
setStatusMessage("Connecting...");
setIndicatorColor(IndicatorColor.RED);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import {
BaseModalDescription,
BaseModalTitle,
@@ -6,7 +5,6 @@ import {
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { BrandButton } from "../settings/brand-button";
import { I18nKey } from "#/i18n/declaration";
interface ConfirmDeleteModalProps {
onConfirm: () => void;
@@ -17,16 +15,12 @@ export function ConfirmDeleteModal({
onConfirm,
onCancel,
}: ConfirmDeleteModalProps) {
const { t } = useTranslation();
return (
<ModalBackdrop>
<ModalBody className="items-start border border-tertiary">
<div className="flex flex-col gap-2">
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_DELETE)} />
<BaseModalDescription
description={t(I18nKey.CONVERSATION$DELETE_WARNING)}
/>
<BaseModalTitle title="Are you sure you want to delete this project?" />
<BaseModalDescription description="All data associated with this project will be lost." />
</div>
<div
className="flex flex-col gap-2 w-full"
@@ -37,18 +31,16 @@ export function ConfirmDeleteModal({
variant="primary"
onClick={onConfirm}
className="w-full"
data-testid="confirm-button"
>
{t(I18nKey.ACTION$CONFIRM)}
Confirm
</BrandButton>
<BrandButton
type="button"
variant="secondary"
onClick={onCancel}
className="w-full"
data-testid="cancel-button"
>
{t(I18nKey.BUTTON$CANCEL)}
Cancel
</BrandButton>
</div>
</ModalBody>
@@ -1,7 +1,6 @@
import React from "react";
import { useSelector } from "react-redux";
import posthog from "posthog-js";
import { useTranslation } from "react-i18next";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationRepoLink } from "./conversation-repo-link";
import {
@@ -13,7 +12,6 @@ import { ConversationCardContextMenu } from "./conversation-card-context-menu";
import { cn } from "#/utils/utils";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
interface ConversationCardProps {
onClick?: () => void;
@@ -48,7 +46,6 @@ export function ConversationCard({
variant = "default",
conversationId,
}: ConversationCardProps) {
const { t } = useTranslation();
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
@@ -223,18 +220,14 @@ export function ConversationCard({
<ConversationRepoLink selectedRepository={selectedRepository} />
)}
<p className="text-xs text-neutral-400">
<span>{t(I18nKey.CONVERSATION$CREATED)} </span>
<span>Created </span>
<time>
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))}{" "}
{t(I18nKey.CONVERSATION$AGO)}
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))} ago
</time>
{showUpdateTime && (
<>
<span>{t(I18nKey.CONVERSATION$UPDATED)} </span>
<time>
{formatTimeDelta(new Date(lastUpdatedAt))}{" "}
{t(I18nKey.CONVERSATION$AGO)}
</time>
<span>, updated </span>
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</>
)}
</p>
@@ -244,7 +237,7 @@ export function ConversationCard({
<BaseModal
isOpen={metricsModalVisible}
onOpenChange={setMetricsModalVisible}
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
title="Metrics Information"
testID="metrics-modal"
>
<div className="space-y-4">
@@ -254,7 +247,7 @@ export function ConversationCard({
{metrics?.cost !== null && (
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span className="text-lg font-semibold">
{t(I18nKey.CONVERSATION$TOTAL_COST)}
Total Cost (USD):
</span>
<span className="font-semibold">
${metrics.cost.toFixed(4)}
@@ -265,7 +258,7 @@ export function ConversationCard({
{metrics?.usage !== null && (
<>
<div className="flex justify-between items-center pb-2">
<span>{t(I18nKey.CONVERSATION$INPUT)}:</span>
<span>Total Input Tokens:</span>
<span className="font-semibold">
{metrics.usage.prompt_tokens.toLocaleString()}
</span>
@@ -283,16 +276,14 @@ export function ConversationCard({
</div>
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span>{t(I18nKey.CONVERSATION$OUTPUT)}:</span>
<span>Total Output Tokens:</span>
<span className="font-semibold">
{metrics.usage.completion_tokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center pt-1">
<span className="font-semibold">
{t(I18nKey.CONVERSATION$TOTAL)}:
</span>
<span className="font-semibold">Total Tokens:</span>
<span className="font-bold">
{(
metrics.usage.prompt_tokens +
@@ -308,9 +299,7 @@ export function ConversationCard({
{!metrics?.cost && !metrics?.usage && (
<div className="rounded-md p-4 text-center">
<p className="text-neutral-400">
{t(I18nKey.CONVERSATION$NO_METRICS)}
</p>
<p className="text-neutral-400">No metrics data available</p>
</div>
)}
</div>
@@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { ModalButton } from "#/components/shared/buttons/modal-button";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { I18nKey } from "#/i18n/declaration";
interface ExitConversationModalProps {
onConfirm: () => void;
@@ -14,20 +12,18 @@ export function ExitConversationModal({
onConfirm,
onClose,
}: ExitConversationModalProps) {
const { t } = useTranslation();
return (
<ModalBackdrop>
<ModalBody testID="confirm-new-conversation-modal">
<BaseModalTitle title={t(I18nKey.CONVERSATION$EXIT_WARNING)} />
<BaseModalTitle title="Creating a new conversation will replace your active conversation" />
<div className="flex w-full gap-2">
<ModalButton
text={t(I18nKey.ACTION$CONFIRM)}
text="Confirm"
onClick={onConfirm}
className="bg-[#C63143] flex-1"
/>
<ModalButton
text={t(I18nKey.BUTTON$CANCEL)}
text="Cancel"
onClick={onClose}
className="bg-tertiary flex-1"
/>
@@ -1,5 +1,3 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import {
BaseModalTitle,
BaseModalDescription,
@@ -19,14 +17,13 @@ export function FeedbackModal({
isOpen,
polarity,
}: FeedbackModalProps) {
const { t } = useTranslation();
if (!isOpen) return null;
return (
<ModalBackdrop onClose={onClose}>
<ModalBody className="border border-tertiary">
<BaseModalTitle title={t(I18nKey.FEEDBACK$TITLE)} />
<BaseModalDescription description={t(I18nKey.FEEDBACK$DESCRIPTION)} />
<BaseModalTitle title="Feedback" />
<BaseModalDescription description="To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data." />
<FeedbackForm onClose={onClose} polarity={polarity} />
</ModalBody>
</ModalBackdrop>
@@ -1,7 +1,5 @@
import React from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { setInitialPrompt } from "#/state/initial-query-slice";
@@ -9,7 +7,6 @@ const INITIAL_PROMPT = "";
export function CodeNotInGitLink() {
const dispatch = useDispatch();
const { t } = useTranslation();
const { mutate: createConversation } = useCreateConversation();
const handleStartFromScratch = () => {
@@ -20,14 +17,14 @@ export function CodeNotInGitLink() {
return (
<div className="text-xs text-neutral-400">
{t(I18nKey.GITHUB$CODE_NOT_IN_GITHUB)}{" "}
Code not in Git?{" "}
<span
onClick={handleStartFromScratch}
className="underline cursor-pointer"
>
{t(I18nKey.GITHUB$START_FROM_SCRATCH)}
Start from scratch
</span>{" "}
{t(I18nKey.GITHUB$VSCODE_LINK_DESCRIPTION)}
and use the VS Code link to upload and download your code.
</div>
);
}
@@ -4,6 +4,7 @@ import { useNavigate } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
import { GitRepositorySelector } from "./git-repo-selector";
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { sanitizeQuery } from "#/utils/sanitize-query";
@@ -30,15 +31,20 @@ export function GitRepositoriesSuggestionBox({
const debouncedSearchQuery = useDebounce(searchQuery, 300);
// TODO: Use `useQueries` to fetch all repositories in parallel
const { data: appRepositories, isLoading: isAppReposLoading } =
useAppRepositories();
const { data: userRepositories, isLoading: isUserReposLoading } =
useUserRepositories();
const { data: searchedRepos, isLoading: isSearchReposLoading } =
useSearchRepositories(sanitizeQuery(debouncedSearchQuery));
const isLoading = isUserReposLoading || isSearchReposLoading;
const isLoading =
isAppReposLoading || isUserReposLoading || isSearchReposLoading;
const repositories =
userRepositories?.pages.flatMap((page) => page.data) || [];
userRepositories?.pages.flatMap((page) => page.data) ||
appRepositories?.pages.flatMap((page) => page.data) ||
[];
const handleConnectToGitHub = () => {
if (gitHubAuthUrl) {
@@ -20,7 +20,7 @@ export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
<div className="flex-1 h-full flex flex-col" style={{ maxWidth }}>
<div
data-testid="jupyter-container"
className="flex-1 overflow-y-auto fast-smooth-scroll"
className="flex-1 overflow-y-auto"
ref={jupyterRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
>
@@ -1,5 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useCreateStripeCheckoutSession } from "#/hooks/mutation/stripe/use-create-stripe-checkout-session";
import { useBalance } from "#/hooks/query/use-balance";
import { cn } from "#/utils/utils";
@@ -8,10 +7,8 @@ import { SettingsInput } from "../settings/settings-input";
import { BrandButton } from "../settings/brand-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { amountIsValid } from "#/utils/amount-is-valid";
import { I18nKey } from "#/i18n/declaration";
export function PaymentForm() {
const { t } = useTranslation();
const { data: balance, isLoading } = useBalance();
const { mutate: addBalance, isPending } = useCreateStripeCheckoutSession();
@@ -41,7 +38,7 @@ export function PaymentForm() {
className="flex flex-col gap-6 px-11 py-9"
>
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
{t(I18nKey.PAYMENT$MANAGE_CREDITS)}
Manage Credits
</h2>
<div
@@ -66,7 +63,7 @@ export function PaymentForm() {
name="top-up-input"
onChange={handleTopUpInputChange}
type="text"
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
label="Add funds"
placeholder="Specify an amount in USD to add - min $10"
className="w-[680px]"
/>
@@ -77,7 +74,7 @@ export function PaymentForm() {
type="submit"
isDisabled={isPending || buttonIsDisabled}
>
{t(I18nKey.PAYMENT$ADD_CREDIT)}
Add credit
</BrandButton>
{isPending && <LoadingSpinner size="small" />}
</div>
@@ -1,6 +1,5 @@
import { useMutation } from "@tanstack/react-query";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
@@ -16,7 +15,7 @@ export function SetupPaymentModal() {
window.location.href = data;
},
onError: () => {
displayErrorToast(t(I18nKey.BILLING$ERROR_WHILE_CREATING_SESSION));
displayErrorToast(t("BILLING$ERROR_WHILE_CREATING_SESSION"));
},
});
@@ -25,9 +24,7 @@ export function SetupPaymentModal() {
<ModalBody className="border border-tertiary">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{t(I18nKey.BILLING$YOUVE_GOT_50)}
</h1>
<h1 className="text-2xl font-bold">{t("BILLING$YOUVE_GOT_50")}</h1>
<p>
<Trans
i18nKey="BILLING$CLAIM_YOUR_50"
@@ -43,7 +40,7 @@ export function SetupPaymentModal() {
isDisabled={isPending}
onClick={mutate}
>
{t(I18nKey.BILLING$PROCEED_TO_STRIPE)}
{t("BILLING$PROCEED_TO_STRIPE")}
</BrandButton>
</ModalBody>
</ModalBackdrop>
@@ -1,17 +1,9 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface AvatarProps {
src: string;
}
export function Avatar({ src }: AvatarProps) {
const { t } = useTranslation();
return (
<img
src={src}
alt={t(I18nKey.AVATAR$ALT_TEXT)}
className="w-full h-full rounded-full"
/>
<img src={src} alt="user avatar" className="w-full h-full rounded-full" />
);
}
@@ -3,7 +3,6 @@ import { FaListUl } from "react-icons/fa";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { NavLink, useLocation } from "react-router";
import { useTranslation } from "react-i18next";
import { useGitUser } from "#/hooks/query/use-git-user";
import { UserActions } from "./user-actions";
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
@@ -22,10 +21,8 @@ import { useLogout } from "#/hooks/mutation/use-logout";
import { useConfig } from "#/hooks/query/use-config";
import { cn } from "#/utils/utils";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { I18nKey } from "#/i18n/declaration";
export function Sidebar() {
const { t } = useTranslation();
const location = useLocation();
const dispatch = useDispatch();
const endSession = useEndSession();
@@ -94,8 +91,8 @@ export function Sidebar() {
<ExitProjectButton onClick={handleEndSession} />
<TooltipButton
testId="toggle-conversation-panel"
tooltip={t(I18nKey.SIDEBAR$CONVERSATIONS)}
ariaLabel={t(I18nKey.SIDEBAR$CONVERSATIONS)}
tooltip="Conversations"
ariaLabel="Conversations"
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
>
<FaListUl
@@ -1,5 +1,4 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import ExportIcon from "#/icons/export.svg?react";
@@ -24,19 +23,19 @@ export function TrajectoryActions({
testId="positive-feedback"
onClick={onPositiveFeedback}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_HELPFUL)}
tooltip={t("BUTTON$MARK_HELPFUL")}
/>
<TrajectoryActionButton
testId="negative-feedback"
onClick={onNegativeFeedback}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_NOT_HELPFUL)}
tooltip={t("BUTTON$MARK_NOT_HELPFUL")}
/>
<TrajectoryActionButton
testId="export-trajectory"
onClick={onExportTrajectory}
icon={<ExportIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$EXPORT_CONVERSATION)}
tooltip={t("BUTTON$EXPORT_CONVERSATION")}
/>
</div>
);
@@ -1,9 +1,4 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function JoinWaitlistAnchor() {
const { t } = useTranslation();
return (
<a
href="https://www.all-hands.dev/join-waitlist"
@@ -11,7 +6,7 @@ export function JoinWaitlistAnchor() {
rel="noreferrer"
className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80"
>
{t(I18nKey.WAITLIST$JOIN_WAITLIST)}
Join Waitlist
</a>
);
}
@@ -1,35 +1,34 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface WaitlistMessageProps {
content: "waitlist" | "sign-in";
}
export function WaitlistMessage({ content }: WaitlistMessageProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{content === "sign-in" && t(I18nKey.AUTH$SIGN_IN_WITH_GITHUB)}
{content === "waitlist" && t(I18nKey.WAITLIST$ALMOST_THERE)}
{content === "sign-in" && "Sign in with GitHub"}
{content === "waitlist" && "Just a little longer!"}
</h1>
{content === "sign-in" && (
<p>
{t(I18nKey.LANDING$OR)}{" "}
or{" "}
<a
href="https://www.all-hands.dev/join-waitlist"
target="_blank"
rel="noreferrer noopener"
className="text-blue-500 hover:underline underline-offset-2"
>
{t(I18nKey.WAITLIST$JOIN)}
join the waitlist
</a>{" "}
{t(I18nKey.WAITLIST$IF_NOT_JOINED)}
if you haven&apos;t already
</p>
)}
{content === "waitlist" && (
<p className="text-sm">{t(I18nKey.WAITLIST$PATIENCE_MESSAGE)}</p>
<p className="text-sm">
Thanks for your patience! We&apos;re accepting new members
progressively. If you haven&apos;t joined the waitlist yet, now&apos;s
the time!
</p>
)}
</div>
);
@@ -1,6 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { JoinWaitlistAnchor } from "./join-waitlist-anchor";
import { WaitlistMessage } from "./waitlist-message";
@@ -20,7 +18,6 @@ export function WaitlistModal({
ghTokenIsSet,
githubAuthUrl,
}: WaitlistModalProps) {
const { t } = useTranslation();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const handleGitHubAuth = () => {
@@ -47,7 +44,7 @@ export function WaitlistModal({
className="w-full"
startContent={<GitHubLogo width={20} height={20} />}
>
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
Connect to GitHub
</BrandButton>
)}
{ghTokenIsSet && <JoinWaitlistAnchor />}
@@ -1,5 +1,3 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { TooltipButton } from "./tooltip-button";
@@ -8,11 +6,10 @@ interface AllHandsLogoButtonProps {
}
export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
const { t } = useTranslation();
return (
<TooltipButton
tooltip={t(I18nKey.BRANDING$ALL_HANDS_AI)}
ariaLabel={t(I18nKey.BRANDING$ALL_HANDS_LOGO)}
tooltip="All Hands AI"
ariaLabel="All Hands Logo"
onClick={onClick}
>
<AllHandsLogo width={34} height={34} />
@@ -1,7 +1,5 @@
import { useTranslation } from "react-i18next";
import CheckmarkIcon from "#/icons/checkmark.svg?react";
import CopyIcon from "#/icons/copy.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface CopyToClipboardButtonProps {
isHidden: boolean;
@@ -16,7 +14,6 @@ export function CopyToClipboardButton({
onClick,
mode,
}: CopyToClipboardButtonProps) {
const { t } = useTranslation();
return (
<button
hidden={isHidden}
@@ -24,9 +21,6 @@ export function CopyToClipboardButton({
data-testid="copy-to-clipboard"
type="button"
onClick={onClick}
aria-label={t(
mode === "copy" ? I18nKey.BUTTON$COPY : I18nKey.BUTTON$COPIED,
)}
className="button-base p-1 absolute top-1 right-1"
>
{mode === "copy" && <CopyIcon width={15} height={15} />}
@@ -1,15 +1,11 @@
import { IoIosRefresh } from "react-icons/io";
import { useTranslation } from "react-i18next";
import { IconButton } from "./icon-button";
import { I18nKey } from "#/i18n/declaration";
interface RefreshIconButtonProps {
onClick: () => void;
}
export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
const { t } = useTranslation();
return (
<IconButton
icon={
@@ -19,7 +15,7 @@ export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
/>
}
testId="refresh"
ariaLabel={t(I18nKey.WORKSPACE$REFRESH)}
ariaLabel="Refresh workspace"
onClick={onClick}
/>
);
@@ -1,7 +1,5 @@
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
import { useTranslation } from "react-i18next";
import { IconButton } from "./icon-button";
import { I18nKey } from "#/i18n/declaration";
interface ToggleWorkspaceIconButtonProps {
onClick: () => void;
@@ -12,8 +10,6 @@ export function ToggleWorkspaceIconButton({
onClick,
isHidden,
}: ToggleWorkspaceIconButtonProps) {
const { t } = useTranslation();
return (
<IconButton
icon={
@@ -30,9 +26,7 @@ export function ToggleWorkspaceIconButton({
)
}
testId="toggle"
ariaLabel={
isHidden ? t(I18nKey.WORKSPACE$OPEN) : t(I18nKey.WORKSPACE$CLOSE)
}
ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
onClick={onClick}
/>
);
@@ -20,7 +20,7 @@ export function BaseUrlInput({ isDisabled, defaultValue }: BaseUrlInputProps) {
id="base-url"
name="base-url"
defaultValue={defaultValue}
aria-label={t(I18nKey.SETTINGS_FORM$BASE_URL)}
aria-label="Base URL"
classNames={{
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}
@@ -1,11 +1,9 @@
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { useEndSession } from "#/hooks/use-end-session";
import { setCurrentAgentState } from "#/state/agent-slice";
import { AgentState } from "#/types/agent-state";
import { DangerModal } from "./confirmation-modals/danger-modal";
import { ModalBackdrop } from "./modal-backdrop";
import { I18nKey } from "#/i18n/declaration";
interface ExitProjectConfirmationModalProps {
onClose: () => void;
@@ -14,7 +12,6 @@ interface ExitProjectConfirmationModalProps {
export function ExitProjectConfirmationModal({
onClose,
}: ExitProjectConfirmationModalProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const endSession = useEndSession();
@@ -27,15 +24,15 @@ export function ExitProjectConfirmationModal({
return (
<ModalBackdrop onClose={onClose}>
<DangerModal
title={t(I18nKey.EXIT_PROJECT$CONFIRM)}
description={t(I18nKey.EXIT_PROJECT$WARNING)}
title="Are you sure you want to exit?"
description="You will lose any unsaved information."
buttons={{
danger: {
text: t(I18nKey.EXIT_PROJECT$TITLE),
text: "Exit Project",
onClick: handleEndSession,
},
cancel: {
text: t(I18nKey.BUTTON$CANCEL),
text: "Cancel",
onClick: onClose,
},
}}
@@ -131,10 +131,7 @@ function SecurityInvariant() {
{t(I18nKey.INVARIANT$EXPORT_TRACE_LABEL)}
</Button>
</div>
<div
className="flex-1 p-4 max-h-screen overflow-y-auto fast-smooth-scroll"
ref={logsRef}
>
<div className="flex-1 p-4 max-h-screen overflow-y-auto" ref={logsRef}>
{logs.map((log: SecurityAnalyzerLog, index: number) => (
<div
key={index}
@@ -93,7 +93,7 @@ export function ModelSelector({
},
}}
>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
<AutocompleteSection title="Verified">
{Object.keys(models)
.filter((provider) => VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
@@ -105,7 +105,7 @@ export function ModelSelector({
</AutocompleteItem>
))}
</AutocompleteSection>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
<AutocompleteSection title="Others">
{Object.keys(models)
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
@@ -143,14 +143,14 @@ export function ModelSelector({
},
}}
>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
<AutocompleteSection title="Verified">
{models[selectedProvider || ""]?.models
.filter((model) => VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem key={model}>{model}</AutocompleteItem>
))}
</AutocompleteSection>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
<AutocompleteSection title="Others">
{models[selectedProvider || ""]?.models
.filter((model) => !VERIFIED_MODELS.includes(model))
.map((model) => (
@@ -93,7 +93,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
<SettingsInput
testId="llm-api-key-input"
name="llm-api-key-input"
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
label="API Key"
type="password"
className="w-[680px]"
placeholder={isLLMKeySet ? "<hidden>" : ""}
@@ -102,8 +102,8 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
<HelpLink
testId="llm-api-key-help-anchor"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
</div>
@@ -30,14 +30,13 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
{t(I18nKey.AI_SETTINGS$TITLE)}
</span>
<p className="text-xs text-[#A3A3A3]">
{t(I18nKey.SETTINGS$DESCRIPTION)}{" "}
{t(I18nKey.SETTINGS$FOR_OTHER_OPTIONS)}
{t(I18nKey.SETTINGS$DESCRIPTION)} For other options,{" "}
<Link
data-testid="advanced-settings-link"
to="/settings"
className="underline underline-offset-2 text-white"
>
{t(I18nKey.SETTINGS$SEE_ADVANCED_SETTINGS)}
see advanced settings
</Link>
</p>
{aiConfigOptions.isLoading && (
@@ -24,11 +24,7 @@ export function ConversationProvider({
const value = useMemo(() => ({ conversationId }), [conversationId]);
return (
<ConversationContext.Provider value={value}>
{children}
</ConversationContext.Provider>
);
return <ConversationContext value={value}>{children}</ConversationContext>;
}
export function useConversation() {
@@ -9,7 +9,6 @@ import {
AssistantMessageAction,
UserMessageAction,
} from "#/types/core/actions";
import { useAuth } from "./auth-context";
const isOpenHandsEvent = (event: unknown): event is OpenHandsParsedEvent =>
typeof event === "object" &&
@@ -111,7 +110,6 @@ export function WsClientProvider({
);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
const { providerTokensSet } = useAuth();
const messageRateHandler = useRate({ threshold: 250 });
@@ -170,7 +168,6 @@ export function WsClientProvider({
const query = {
latest_event_id: lastEvent?.id ?? -1,
conversation_id: conversationId,
providers_set: providerTokensSet,
};
const baseUrl =
@@ -20,7 +20,7 @@ export const useCreateConversation = () => {
if (variables.q) dispatch(setInitialPrompt(variables.q));
return OpenHands.createConversation(
selectedRepository || undefined,
selectedRepository?.full_name || undefined,
variables.q,
files,
replayJson || undefined,
@@ -0,0 +1,20 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
export const useAppInstallations = () => {
const { data: config } = useConfig();
const { providersAreSet } = useAuth();
return useQuery({
queryKey: ["installations", providersAreSet, config?.GITHUB_CLIENT_ID],
queryFn: OpenHands.getGitHubUserInstallationIds,
enabled:
providersAreSet &&
!!config?.GITHUB_CLIENT_ID &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};
@@ -0,0 +1,67 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import React from "react";
import { retrieveGitHubAppRepositories } from "#/api/git";
import { useAppInstallations } from "./use-app-installations";
import { useConfig } from "./use-config";
import { useAuth } from "#/context/auth-context";
export const useAppRepositories = () => {
const { providersAreSet } = useAuth();
const { data: config } = useConfig();
const { data: installations } = useAppInstallations();
const repos = useInfiniteQuery({
queryKey: ["repositories", providersAreSet, installations],
queryFn: async ({
pageParam,
}: {
pageParam: { installationIndex: number | null; repoPage: number | null };
}) => {
const { repoPage, installationIndex } = pageParam;
if (!installations) {
throw new Error("Missing installation list");
}
return retrieveGitHubAppRepositories(
installationIndex || 0,
installations,
repoPage || 1,
30,
);
},
initialPageParam: { installationIndex: 0, repoPage: 1 },
getNextPageParam: (lastPage) => {
if (lastPage.nextPage) {
return {
installationIndex: lastPage.installationIndex,
repoPage: lastPage.nextPage,
};
}
if (lastPage.installationIndex !== null) {
return { installationIndex: lastPage.installationIndex, repoPage: 1 };
}
return null;
},
enabled:
providersAreSet &&
Array.isArray(installations) &&
installations.length > 0 &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
// (nextui autocomplete doesn't support onEndReached nor is it compatible for extending)
const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos;
React.useEffect(() => {
if (!isFetchingNextPage && isSuccess && hasNextPage) {
fetchNextPage();
}
}, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]);
return repos;
};
+1 -10
View File
@@ -69,18 +69,9 @@ export const useSettings = () => {
// that would prepopulate the data to the cache and mess with expectations. Read more:
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
if (query.error?.status === 404) {
// Create a new object with only the properties we need, avoiding rest destructuring
return {
...query,
data: DEFAULT_SETTINGS,
error: query.error,
isError: query.isError,
isLoading: query.isLoading,
isFetching: query.isFetching,
isFetched: query.isFetched,
isSuccess: query.isSuccess,
status: query.status,
fetchStatus: query.fetchStatus,
refetch: query.refetch,
};
}
@@ -1,17 +1,20 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import React from "react";
import { retrieveUserGitRepositories } from "#/api/git";
import { useConfig } from "./use-config";
import { useAuth } from "#/context/auth-context";
export const useUserRepositories = () => {
const { providerTokensSet, providersAreSet } = useAuth();
const { data: config } = useConfig();
const repos = useInfiniteQuery({
queryKey: ["repositories", providerTokensSet],
queryFn: async () => retrieveUserGitRepositories(),
queryFn: async ({ pageParam }) =>
retrieveUserGitRepositories(pageParam, 100),
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: providersAreSet,
enabled: providersAreSet && config?.APP_MODE === "oss",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
@@ -18,7 +18,7 @@ export function useDocumentTitleFromState(suffix = "OpenHands") {
useEffect(() => {
if (conversation?.title) {
lastValidTitleRef.current = conversation.title;
document.title = `${conversation.title} | ${suffix}`;
document.title = `${conversation.title} - ${suffix}`;
} else {
document.title = suffix;
}
+18 -47
View File
@@ -1,70 +1,41 @@
import { RefObject, useEffect, useState, useCallback } from "react";
import { RefObject, useEffect, useState } from "react";
export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
// Track whether we should auto-scroll to the bottom when content changes
const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true);
// for auto-scroll
// Track whether the user is currently at the bottom of the scroll area
const [autoScroll, setAutoScroll] = useState(true);
const [hitBottom, setHitBottom] = useState(true);
// Check if the scroll position is at the bottom
const isAtBottom = useCallback((element: HTMLElement): boolean => {
const bottomThreshold = 10; // Pixels from bottom to consider "at bottom"
const bottomPosition = element.scrollTop + element.clientHeight;
return bottomPosition >= element.scrollHeight - bottomThreshold;
}, []);
const onChatBodyScroll = (e: HTMLElement) => {
const bottomHeight = e.scrollTop + e.clientHeight;
// Handle scroll events
const onChatBodyScroll = useCallback(
(e: HTMLElement) => {
const isCurrentlyAtBottom = isAtBottom(e);
setHitBottom(isCurrentlyAtBottom);
const isHitBottom = bottomHeight >= e.scrollHeight - 10;
// Only update shouldScrollToBottom when user manually scrolls
// This prevents content changes from affecting our scroll behavior decision
setShouldScrollToBottom(isCurrentlyAtBottom);
},
[isAtBottom],
);
setHitBottom(isHitBottom);
setAutoScroll(isHitBottom);
};
// Scroll to bottom function with animation
const scrollDomToBottom = useCallback(() => {
function scrollDomToBottom() {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
// Set shouldScrollToBottom to true when manually scrolling to bottom
setShouldScrollToBottom(true);
setHitBottom(true);
// Use smooth scrolling but with a fast duration
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
});
setAutoScroll(true);
dom.scrollTo({ top: dom.scrollHeight, behavior: "auto" });
});
}
}, [scrollRef]);
}
// Auto-scroll effect that runs when content changes
// auto scroll
useEffect(() => {
// Only auto-scroll if the user was already at the bottom
if (shouldScrollToBottom) {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
});
});
}
if (autoScroll) {
scrollDomToBottom();
}
});
return {
scrollRef,
autoScroll: shouldScrollToBottom,
setAutoScroll: setShouldScrollToBottom,
autoScroll,
setAutoScroll,
scrollDomToBottom,
hitBottom,
setHitBottom,
-87
View File
@@ -1,19 +1,5 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
SETTINGS$ADVANCED = "SETTINGS$ADVANCED",
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
SETTINGS$AGENT = "SETTINGS$AGENT",
SETTINGS$ENABLE_MEMORY_CONDENSATION = "SETTINGS$ENABLE_MEMORY_CONDENSATION",
SETTINGS$LANGUAGE = "SETTINGS$LANGUAGE",
ACTION$PUSH_TO_BRANCH = "ACTION$PUSH_TO_BRANCH",
ACTION$PUSH_CREATE_PR = "ACTION$PUSH_CREATE_PR",
ACTION$PUSH_CHANGES_TO_PR = "ACTION$PUSH_CHANGES_TO_PR",
ANALYTICS$TITLE = "ANALYTICS$TITLE",
ANALYTICS$DESCRIPTION = "ANALYTICS$DESCRIPTION",
ANALYTICS$SEND_ANONYMOUS_DATA = "ANALYTICS$SEND_ANONYMOUS_DATA",
ANALYTICS$CONFIRM_PREFERENCES = "ANALYTICS$CONFIRM_PREFERENCES",
BUTTON$COPY = "BUTTON$COPY",
BUTTON$COPIED = "BUTTON$COPIED",
APP$TITLE = "APP$TITLE",
BROWSER$TITLE = "BROWSER$TITLE",
BROWSER$EMPTY_MESSAGE = "BROWSER$EMPTY_MESSAGE",
@@ -38,25 +24,11 @@ export enum I18nKey {
SUGGESTIONS$AUTO_MERGE_PRS = "SUGGESTIONS$AUTO_MERGE_PRS",
SUGGESTIONS$FIX_README = "SUGGESTIONS$FIX_README",
SUGGESTIONS$CLEAN_DEPENDENCIES = "SUGGESTIONS$CLEAN_DEPENDENCIES",
SETTINGS$LLM_SETTINGS = "SETTINGS$LLM_SETTINGS",
SETTINGS$GITHUB_SETTINGS = "SETTINGS$GITHUB_SETTINGS",
SETTINGS$SOUND_NOTIFICATIONS = "SETTINGS$SOUND_NOTIFICATIONS",
SETTINGS$CUSTOM_MODEL = "SETTINGS$CUSTOM_MODEL",
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
AVATAR$ALT_TEXT = "AVATAR$ALT_TEXT",
BRANDING$ALL_HANDS_AI = "BRANDING$ALL_HANDS_AI",
BRANDING$ALL_HANDS_LOGO = "BRANDING$ALL_HANDS_LOGO",
ERROR$GENERIC = "ERROR$GENERIC",
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
WORKSPACE$PLANNER_TAB_LABEL = "WORKSPACE$PLANNER_TAB_LABEL",
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
WORKSPACE$REFRESH = "WORKSPACE$REFRESH",
WORKSPACE$OPEN = "WORKSPACE$OPEN",
WORKSPACE$CLOSE = "WORKSPACE$CLOSE",
VSCODE$OPEN = "VSCODE$OPEN",
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
@@ -76,8 +48,6 @@ export enum I18nKey {
MODAL$END_SESSION_MESSAGE = "MODAL$END_SESSION_MESSAGE",
BUTTON$END_SESSION = "BUTTON$END_SESSION",
BUTTON$CANCEL = "BUTTON$CANCEL",
EXIT_PROJECT$CONFIRM = "EXIT_PROJECT$CONFIRM",
EXIT_PROJECT$TITLE = "EXIT_PROJECT$TITLE",
LANGUAGE$LABEL = "LANGUAGE$LABEL",
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
@@ -206,12 +176,6 @@ export enum I18nKey {
LANDING$REPLAY = "LANDING$REPLAY",
LANDING$UPLOAD_TRAJECTORY = "LANDING$UPLOAD_TRAJECTORY",
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
CONVERSATION$CREATED = "CONVERSATION$CREATED",
CONVERSATION$AGO = "CONVERSATION$AGO",
GITHUB$VSCODE_LINK_DESCRIPTION = "GITHUB$VSCODE_LINK_DESCRIPTION",
CONVERSATION$EXIT_WARNING = "CONVERSATION$EXIT_WARNING",
LANDING$OR = "LANDING$OR",
SUGGESTIONS$TEST_COVERAGE = "SUGGESTIONS$TEST_COVERAGE",
SUGGESTIONS$AUTO_MERGE = "SUGGESTIONS$AUTO_MERGE",
@@ -252,14 +216,9 @@ export enum I18nKey {
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
SETTINGS$SAVED = "SETTINGS$SAVED",
SETTINGS$RESET = "SETTINGS$RESET",
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
FEEDBACK$PRIVATE_LABEL = "FEEDBACK$PRIVATE_LABEL",
SIDEBAR$CONVERSATIONS = "SIDEBAR$CONVERSATIONS",
STATUS$STARTING_RUNTIME = "STATUS$STARTING_RUNTIME",
STATUS$STARTING_CONTAINER = "STATUS$STARTING_CONTAINER",
STATUS$PREPARING_CONTAINER = "STATUS$PREPARING_CONTAINER",
@@ -331,7 +290,6 @@ export enum I18nKey {
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
@@ -362,49 +320,4 @@ export enum I18nKey {
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
BILLING$YOURE_IN = "BILLING$YOURE_IN",
PAYMENT$ADD_FUNDS = "PAYMENT$ADD_FUNDS",
PAYMENT$ADD_CREDIT = "PAYMENT$ADD_CREDIT",
PAYMENT$MANAGE_CREDITS = "PAYMENT$MANAGE_CREDITS",
AUTH$SIGN_IN_WITH_GITHUB = "AUTH$SIGN_IN_WITH_GITHUB",
WAITLIST$JOIN = "WAITLIST$JOIN",
WAITLIST$IF_NOT_JOINED = "WAITLIST$IF_NOT_JOINED",
WAITLIST$PATIENCE_MESSAGE = "WAITLIST$PATIENCE_MESSAGE",
WAITLIST$ALMOST_THERE = "WAITLIST$ALMOST_THERE",
PAYMENT$SUCCESS = "PAYMENT$SUCCESS",
PAYMENT$CANCELLED = "PAYMENT$CANCELLED",
SERVED_APP$TITLE = "SERVED_APP$TITLE",
CONVERSATION$UNKNOWN = "CONVERSATION$UNKNOWN",
SETTINGS$RUNTIME_OPTION_1X = "SETTINGS$RUNTIME_OPTION_1X",
SETTINGS$RUNTIME_OPTION_2X = "SETTINGS$RUNTIME_OPTION_2X",
SETTINGS$GET_IN_TOUCH = "SETTINGS$GET_IN_TOUCH",
CONVERSATION$NO_METRICS = "CONVERSATION$NO_METRICS",
CONVERSATION$DOWNLOAD_ERROR = "CONVERSATION$DOWNLOAD_ERROR",
CONVERSATION$UPDATED = "CONVERSATION$UPDATED",
CONVERSATION$TOTAL_COST = "CONVERSATION$TOTAL_COST",
CONVERSATION$TOKENS_USED = "CONVERSATION$TOKENS_USED",
CONVERSATION$INPUT = "CONVERSATION$INPUT",
CONVERSATION$OUTPUT = "CONVERSATION$OUTPUT",
CONVERSATION$TOTAL = "CONVERSATION$TOTAL",
SETTINGS$RUNTIME_SETTINGS = "SETTINGS$RUNTIME_SETTINGS",
SETTINGS$RESET_CONFIRMATION = "SETTINGS$RESET_CONFIRMATION",
ERROR$GENERIC_OOPS = "ERROR$GENERIC_OOPS",
ERROR$UNKNOWN = "ERROR$UNKNOWN",
SETTINGS$FOR_OTHER_OPTIONS = "SETTINGS$FOR_OTHER_OPTIONS",
SETTINGS$SEE_ADVANCED_SETTINGS = "SETTINGS$SEE_ADVANCED_SETTINGS",
SETTINGS_FORM$API_KEY = "SETTINGS_FORM$API_KEY",
SETTINGS_FORM$BASE_URL = "SETTINGS_FORM$BASE_URL",
GITHUB$CONNECT_TO_GITHUB = "GITHUB$CONNECT_TO_GITHUB",
WAITLIST$JOIN_WAITLIST = "WAITLIST$JOIN_WAITLIST",
ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS = "ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS",
ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB = "ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB",
CONVERSATION$DELETE_WARNING = "CONVERSATION$DELETE_WARNING",
FEEDBACK$TITLE = "FEEDBACK$TITLE",
FEEDBACK$DESCRIPTION = "FEEDBACK$DESCRIPTION",
EXIT_PROJECT$WARNING = "EXIT_PROJECT$WARNING",
MODEL_SELECTOR$VERIFIED = "MODEL_SELECTOR$VERIFIED",
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
GITLAB$OR_SEE = "GITLAB$OR_SEE",
COMMON$DOCUMENTATION = "COMMON$DOCUMENTATION",
}
File diff suppressed because it is too large Load Diff
-6
View File
@@ -57,9 +57,3 @@ code {
.markdown-body td {
padding: 0.1rem 1rem;
}
/* Fast smooth scrolling for chat interface */
.fast-smooth-scroll {
scroll-behavior: smooth;
scroll-timeline: 100ms;
}
+3 -5
View File
@@ -3,8 +3,6 @@ import {
QueryCache,
MutationCache,
} from "@tanstack/react-query";
import i18next from "i18next";
import { I18nKey } from "./i18n/declaration";
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
import { displayErrorToast } from "./utils/custom-toast-handlers";
@@ -15,8 +13,8 @@ export const queryClientConfig: QueryClientConfig = {
if (!query.meta?.disableToast) {
const errorMessage = retrieveAxiosErrorMessage(error);
if (!shownErrors.has(errorMessage || "")) {
displayErrorToast(errorMessage || i18next.t(I18nKey.ERROR$GENERIC));
if (!shownErrors.has(errorMessage)) {
displayErrorToast(errorMessage || "An error occurred");
shownErrors.add(errorMessage);
setTimeout(() => {
@@ -30,7 +28,7 @@ export const queryClientConfig: QueryClientConfig = {
onError: (error, _, __, mutation) => {
if (!mutation?.meta?.disableToast) {
const message = retrieveAxiosErrorMessage(error);
displayErrorToast(message || i18next.t(I18nKey.ERROR$GENERIC));
displayErrorToast(message);
}
},
}),
+1 -3
View File
@@ -2,17 +2,15 @@ import React from "react";
import { useRouteError } from "react-router";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useTranslation } from "react-i18next";
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
import { useFiles } from "#/context/files";
export function ErrorBoundary() {
const error = useRouteError();
const { t } = useTranslation();
return (
<div className="w-full h-full border border-danger rounded-b-xl flex flex-col items-center justify-center gap-2 bg-red-500/5">
<h1 className="text-3xl font-bold">{t("ERROR$GENERIC")}</h1>
<h1 className="text-3xl font-bold">Oops! An error occurred!</h1>
{error instanceof Error && <pre>{error.message}</pre>}
</div>
);
-4
View File
@@ -36,7 +36,6 @@ import { useSettings } from "#/hooks/query/use-settings";
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
function AppContent() {
useConversationConfig();
@@ -52,9 +51,6 @@ function AppContent() {
const dispatch = useDispatch();
const endSession = useEndSession();
// Set the document title to the conversation title when available
useDocumentTitleFromState();
const [width, setWidth] = React.useState(window.innerWidth);
const secrets = React.useMemo(
+3 -5
View File
@@ -8,7 +8,6 @@ import {
useSearchParams,
} from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import i18n from "#/i18n";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
@@ -25,7 +24,6 @@ import { displaySuccessToast } from "#/utils/custom-toast-handlers";
export function ErrorBoundary() {
const error = useRouteError();
const { t } = useTranslation();
if (isRouteErrorResponse(error)) {
return (
@@ -43,7 +41,7 @@ export function ErrorBoundary() {
if (error instanceof Error) {
return (
<div>
<h1>{t(I18nKey.ERROR$GENERIC)}</h1>
<h1>Uh oh, an error occurred!</h1>
<pre>{error.message}</pre>
</div>
);
@@ -51,7 +49,7 @@ export function ErrorBoundary() {
return (
<div>
<h1>{t(I18nKey.ERROR$UNKNOWN)}</h1>
<h1>Uh oh, an unknown error occurred!</h1>
</div>
);
}
@@ -107,7 +105,7 @@ export default function MainApp() {
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (!isFetching && searchParams.get("free_credits") === "success") {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
displaySuccessToast(t("BILLING$YOURE_IN"));
searchParams.delete("free_credits");
navigate("/");
}
+38 -44
View File
@@ -1,7 +1,5 @@
import React from "react";
import { Link } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "#/components/features/settings/brand-button";
import { HelpLink } from "#/components/features/settings/help-link";
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
@@ -30,15 +28,12 @@ import {
import { ProviderOptions } from "#/types/settings";
import { useAuth } from "#/context/auth-context";
// Define REMOTE_RUNTIME_OPTIONS for testing
const REMOTE_RUNTIME_OPTIONS = [
{ key: "1", label: "Standard" },
{ key: "2", label: "Enhanced" },
{ key: "4", label: "Premium" },
{ key: 1, label: "1x (2 core, 8G)" },
{ key: 2, label: "2x (4 core, 16G)" },
];
function AccountSettings() {
const { t } = useTranslation();
const {
data: settings,
isFetching: isFetchingSettings,
@@ -161,21 +156,20 @@ function AccountSettings() {
SECURITY_ANALYZER:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor !== null
? Number(remoteRuntimeResourceFactor)
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
remoteRuntimeResourceFactor ||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
};
saveSettings(newSettings, {
onSuccess: () => {
handleCaptureConsent(userConsentsToAnalytics);
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
displaySuccessToast("Settings saved");
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
displayErrorToast(errorMessage);
},
});
};
@@ -183,7 +177,7 @@ function AccountSettings() {
const handleReset = () => {
saveSettings(null, {
onSuccess: () => {
displaySuccessToast(t(I18nKey.SETTINGS$RESET));
displaySuccessToast("Settings reset");
setResetSettingsModalIsOpen(false);
setLlmConfigMode("basic");
},
@@ -233,7 +227,7 @@ function AccountSettings() {
>
<div className="flex items-center gap-7">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
{t(I18nKey.SETTINGS$LLM_SETTINGS)}
LLM Settings
</h2>
{!shouldHandleSpecialSaasCase && (
<SettingsSwitch
@@ -241,7 +235,7 @@ function AccountSettings() {
defaultIsToggled={isAdvancedSettingsSet}
onToggle={onToggleAdvancedMode}
>
{t(I18nKey.SETTINGS$ADVANCED)}
Advanced
</SettingsSwitch>
)}
</div>
@@ -257,7 +251,7 @@ function AccountSettings() {
<SettingsInput
testId="llm-custom-model-input"
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
label="Custom Model"
defaultValue={settings.LLM_MODEL}
placeholder="anthropic/claude-3-5-sonnet-20241022"
type="text"
@@ -268,7 +262,7 @@ function AccountSettings() {
<SettingsInput
testId="base-url-input"
name="base-url-input"
label={t(I18nKey.SETTINGS$BASE_URL)}
label="Base URL"
defaultValue={settings.LLM_BASE_URL}
placeholder="https://api.openai.com"
type="text"
@@ -280,7 +274,7 @@ function AccountSettings() {
<SettingsInput
testId="llm-api-key-input"
name="llm-api-key-input"
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
label="API Key"
type="password"
className="w-[680px]"
placeholder={isLLMKeySet ? "<hidden>" : ""}
@@ -293,8 +287,8 @@ function AccountSettings() {
{!shouldHandleSpecialSaasCase && (
<HelpLink
testId="llm-api-key-help-anchor"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
)}
@@ -303,7 +297,7 @@ function AccountSettings() {
<SettingsDropdownInput
testId="agent-input"
name="agent-input"
label={t(I18nKey.SETTINGS$AGENT)}
label="Agent"
items={
resources?.agents.map((agent) => ({
key: agent,
@@ -321,9 +315,9 @@ function AccountSettings() {
name="runtime-settings-input"
label={
<>
{t(I18nKey.SETTINGS$RUNTIME_SETTINGS)}
Runtime Settings (
<a href="mailto:contact@all-hands.dev">
{t(I18nKey.SETTINGS$GET_IN_TOUCH)}
get in touch for access
</a>
)
</>
@@ -342,7 +336,7 @@ function AccountSettings() {
defaultIsToggled={!!settings.CONFIRMATION_MODE}
isBeta
>
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
Enable confirmation mode
</SettingsSwitch>
)}
@@ -352,7 +346,7 @@ function AccountSettings() {
name="enable-memory-condenser-switch"
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
>
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
Enable memory condensation
</SettingsSwitch>
)}
@@ -361,7 +355,7 @@ function AccountSettings() {
<SettingsDropdownInput
testId="security-analyzer-input"
name="security-analyzer-input"
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
label="Security Analyzer"
items={
resources?.securityAnalyzers.map((analyzer) => ({
key: analyzer,
@@ -379,7 +373,7 @@ function AccountSettings() {
<section className="flex flex-col gap-6">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
{t(I18nKey.SETTINGS$GITHUB_SETTINGS)}
Git Provider Settings
</h2>
{isSaas && hasAppSlug && (
<Link
@@ -388,7 +382,7 @@ function AccountSettings() {
rel="noreferrer noopener"
>
<BrandButton type="button" variant="secondary">
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
Configure GitHub Repositories
</BrandButton>
</Link>
)}
@@ -397,7 +391,7 @@ function AccountSettings() {
<SettingsInput
testId="github-token-input"
name="github-token-input"
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
label="GitHub Token"
type="password"
className="w-[680px]"
startContent={
@@ -409,7 +403,7 @@ function AccountSettings() {
/>
<p data-testid="github-token-help-anchor" className="text-xs">
{" "}
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
Generate a token on{" "}
<b>
{" "}
<a
@@ -421,7 +415,7 @@ function AccountSettings() {
GitHub
</a>{" "}
</b>
{t(I18nKey.COMMON$HERE)}{" "}
or see the{" "}
<b>
<a
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
@@ -429,7 +423,7 @@ function AccountSettings() {
className="underline underline-offset-2"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
documentation
</a>
</b>
.
@@ -438,7 +432,7 @@ function AccountSettings() {
<SettingsInput
testId="gitlab-token-input"
name="gitlab-token-input"
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
label="GitLab Token"
type="password"
className="w-[680px]"
startContent={
@@ -449,9 +443,9 @@ function AccountSettings() {
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
/>
<p data-testid="gitlab-token-help-anchor" className="text-xs">
<p data-testId="gitlab-token-help-anchor" className="text-xs">
{" "}
{t(I18nKey.GITLAB$GET_TOKEN)}{" "}
Generate a token on{" "}
<b>
{" "}
<a
@@ -463,7 +457,7 @@ function AccountSettings() {
GitLab
</a>{" "}
</b>
{t(I18nKey.GITLAB$OR_SEE)}{" "}
or see the{" "}
<b>
<a
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
@@ -471,7 +465,7 @@ function AccountSettings() {
className="underline underline-offset-2"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$DOCUMENTATION)}
documentation
</a>
</b>
.
@@ -490,13 +484,13 @@ function AccountSettings() {
<section className="flex flex-col gap-6">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
{t(I18nKey.ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS)}
Additional Settings
</h2>
<SettingsDropdownInput
testId="language-input"
name="language-input"
label={t(I18nKey.SETTINGS$LANGUAGE)}
label="Language"
items={AvailableLanguages.map((language) => ({
key: language.value,
label: language.label,
@@ -510,7 +504,7 @@ function AccountSettings() {
name="enable-analytics-switch"
defaultIsToggled={!!isAnalyticsEnabled}
>
{t(I18nKey.ANALYTICS$ENABLE)}
Enable analytics
</SettingsSwitch>
<SettingsSwitch
@@ -518,7 +512,7 @@ function AccountSettings() {
name="enable-sound-notifications-switch"
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
>
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
Enable sound notifications
</SettingsSwitch>
</section>
</div>
@@ -530,7 +524,7 @@ function AccountSettings() {
variant="secondary"
onClick={() => setResetSettingsModalIsOpen(true)}
>
{t(I18nKey.BUTTON$RESET_TO_DEFAULTS)}
Reset to defaults
</BrandButton>
<BrandButton
type="button"
@@ -539,7 +533,7 @@ function AccountSettings() {
formRef.current?.requestSubmit();
}}
>
{t(I18nKey.BUTTON$SAVE)}
Save Changes
</BrandButton>
</footer>
@@ -549,7 +543,7 @@ function AccountSettings() {
data-testid="reset-modal"
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
>
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
<p>Are you sure you want to reset all settings?</p>
<div className="w-full flex gap-2">
<BrandButton
type="button"
+1 -4
View File
@@ -1,13 +1,10 @@
import React from "react";
import { FaArrowRotateRight } from "react-icons/fa6";
import { FaExternalLinkAlt, FaHome } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { useActiveHost } from "#/hooks/query/use-active-host";
import { PathForm } from "#/components/features/served-host/path-form";
import { I18nKey } from "#/i18n/declaration";
function ServedApp() {
const { t } = useTranslation();
const { activeHost } = useActiveHost();
const [refreshKey, setRefreshKey] = React.useState(0);
const [currentActiveHost, setCurrentActiveHost] = React.useState<
@@ -87,7 +84,7 @@ function ServedApp() {
</div>
<iframe
key={refreshKey}
title={t(I18nKey.SERVED_APP$TITLE)}
title="Served App"
src={fullUrl}
className="w-full h-full"
/>
+2 -5
View File
@@ -1,6 +1,5 @@
import { redirect, useSearchParams } from "react-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { PaymentForm } from "#/components/features/payment/payment-form";
import { GetConfigResponse } from "#/api/open-hands.types";
import { queryClient } from "#/entry.client";
@@ -8,7 +7,6 @@ import {
displayErrorToast,
displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { I18nKey } from "#/i18n/declaration";
export const clientLoader = async () => {
const config = queryClient.getQueryData<GetConfigResponse>(["config"]);
@@ -21,15 +19,14 @@ export const clientLoader = async () => {
};
function BillingSettingsScreen() {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const checkoutStatus = searchParams.get("checkout");
React.useEffect(() => {
if (checkoutStatus === "success") {
displaySuccessToast(t(I18nKey.PAYMENT$SUCCESS));
displaySuccessToast("Payment successful");
} else if (checkoutStatus === "cancel") {
displayErrorToast(t(I18nKey.PAYMENT$CANCELLED));
displayErrorToast("Payment cancelled");
}
setSearchParams({});
+1 -4
View File
@@ -1,12 +1,9 @@
import { NavLink, Outlet } from "react-router";
import { useTranslation } from "react-i18next";
import SettingsIcon from "#/icons/settings.svg?react";
import { cn } from "#/utils/utils";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
function SettingsScreen() {
const { t } = useTranslation();
const { data: config } = useConfig();
const isSaas = config?.APP_MODE === "saas";
const billingIsEnabled = config?.FEATURE_FLAGS.ENABLE_BILLING;
@@ -18,7 +15,7 @@ function SettingsScreen() {
>
<header className="px-3 py-1.5 border-b border-b-tertiary flex items-center gap-2">
<SettingsIcon width={16} height={16} />
<h1 className="text-sm leading-6">{t(I18nKey.SETTINGS$TITLE)}</h1>
<h1 className="text-sm leading-6">Settings</h1>
</header>
{isSaas && billingIsEnabled && (
-16
View File
@@ -51,7 +51,6 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.EDIT:
case ObservationType.THINK:
case ObservationType.NULL:
case ObservationType.RECALL:
break; // We don't display the default message for these observations
default:
store.dispatch(addAssistantMessage(message.message));
@@ -77,21 +76,6 @@ export function handleObservationMessage(message: ObservationMessage) {
}),
);
break;
case "recall":
store.dispatch(
addAssistantObservation({
...baseObservation,
observation: "recall" as const,
extras: {
...(message.extras || {}),
recall_type:
(message.extras?.recall_type as
| "workspace_context"
| "knowledge") || "knowledge",
},
}),
);
break;
case "run":
store.dispatch(
addAssistantObservation({
-72
View File
@@ -6,7 +6,6 @@ import {
OpenHandsObservation,
CommandObservation,
IPythonObservation,
RecallObservation,
} from "#/types/core/observations";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";
@@ -23,7 +22,6 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
"browse",
"browse_interactive",
"edit",
"recall",
];
function getRiskText(risk: ActionSecurityRisk) {
@@ -114,9 +112,6 @@ export const chatSlice = createSlice({
} else if (actionID === "browse_interactive") {
// Include the browser_actions in the content
text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
} else if (actionID === "recall") {
// skip recall actions
return;
}
if (actionID === "run" || actionID === "run_ipython") {
if (
@@ -148,73 +143,6 @@ export const chatSlice = createSlice({
if (!HANDLED_ACTIONS.includes(observationID)) {
return;
}
// Special handling for RecallObservation - create a new message instead of updating an existing one
if (observationID === "recall") {
const recallObs = observation.payload as RecallObservation;
let content = ``;
// Handle workspace context
if (recallObs.extras.recall_type === "workspace_context") {
if (recallObs.extras.repo_name) {
content += `\n\n**Repository:** ${recallObs.extras.repo_name}`;
}
if (recallObs.extras.repo_directory) {
content += `\n\n**Directory:** ${recallObs.extras.repo_directory}`;
}
if (recallObs.extras.date) {
content += `\n\n**Date:** ${recallObs.extras.date}`;
}
if (
recallObs.extras.runtime_hosts &&
Object.keys(recallObs.extras.runtime_hosts).length > 0
) {
content += `\n\n**Available Hosts**`;
for (const [host, port] of Object.entries(
recallObs.extras.runtime_hosts,
)) {
content += `\n\n- ${host} (port ${port})`;
}
}
if (recallObs.extras.repo_instructions) {
content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
}
if (recallObs.extras.additional_agent_instructions) {
content += `\n\n**Additional Instructions:**\n\n${recallObs.extras.additional_agent_instructions}`;
}
}
// Create a new message for the observation
// Use the correct translation ID format that matches what's in the i18n file
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
// Handle microagent knowledge
if (
recallObs.extras.microagent_knowledge &&
recallObs.extras.microagent_knowledge.length > 0
) {
content += `\n\n**Triggered Microagent Knowledge:**`;
for (const knowledge of recallObs.extras.microagent_knowledge) {
content += `\n\n- **${knowledge.name}** (triggered by keyword: ${knowledge.trigger})\n\n\`\`\`\n${knowledge.content}\n\`\`\``;
}
}
const message: Message = {
type: "action",
sender: "assistant",
translationID,
eventID: observation.payload.id,
content,
imageUrls: [],
timestamp: new Date().toISOString(),
success: true,
};
state.messages.push(message);
return; // Skip the normal observation handling below
}
// Normal handling for other observation types
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
const causeID = observation.payload.cause;
const causeMessage = state.messages.find(
+1 -11
View File
@@ -133,15 +133,6 @@ export interface RejectAction extends OpenHandsActionEvent<"reject"> {
};
}
export interface RecallAction extends OpenHandsActionEvent<"recall"> {
source: "agent";
args: {
recall_type: "workspace_context" | "knowledge";
query: string;
thought: string;
};
}
export type OpenHandsAction =
| UserMessageAction
| AssistantMessageAction
@@ -155,5 +146,4 @@ export type OpenHandsAction =
| FileReadAction
| FileEditAction
| FileWriteAction
| RejectAction
| RecallAction;
| RejectAction;
+1 -2
View File
@@ -12,8 +12,7 @@ export type OpenHandsEventType =
| "reject"
| "think"
| "finish"
| "error"
| "recall";
| "error";
interface OpenHandsBaseEvent {
id: number;
+1 -22
View File
@@ -109,26 +109,6 @@ export interface AgentThinkObservation
};
}
export interface MicroagentKnowledge {
name: string;
trigger: string;
content: string;
}
export interface RecallObservation extends OpenHandsObservationEvent<"recall"> {
source: "agent";
extras: {
recall_type?: "workspace_context" | "knowledge";
repo_name?: string;
repo_directory?: string;
repo_instructions?: string;
runtime_hosts?: Record<string, number>;
additional_agent_instructions?: string;
date?: string;
microagent_knowledge?: MicroagentKnowledge[];
};
}
export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
@@ -140,5 +120,4 @@ export type OpenHandsObservation =
| WriteObservation
| ReadObservation
| EditObservation
| ErrorObservation
| RecallObservation;
| ErrorObservation;
-3
View File
@@ -29,9 +29,6 @@ enum ObservationType {
// A response to the agent's thought (usually a static message)
THINK = "think",
// An observation that shows agent's context extension
RECALL = "recall",
// A no-op observation
NULL = "null",
}
+3 -6
View File
@@ -4,9 +4,6 @@ let titleInterval: number | undefined;
const isBrowser =
typeof window !== "undefined" && typeof document !== "undefined";
// Use a constant for the notification parameter to avoid hardcoded strings
const NOTIFICATION_PARAM = "notification";
export const browserTab = {
startNotification(message: string) {
if (!isBrowser) return;
@@ -32,9 +29,9 @@ export const browserTab = {
'link[rel="icon"]',
) as HTMLLinkElement;
if (favicon) {
favicon.href = favicon.href.includes(`?${NOTIFICATION_PARAM}`)
favicon.href = favicon.href.includes("?notification")
? favicon.href
: `${favicon.href}?${NOTIFICATION_PARAM}`;
: `${favicon.href}?notification`;
}
},
@@ -54,7 +51,7 @@ export const browserTab = {
'link[rel="icon"]',
) as HTMLLinkElement;
if (favicon) {
favicon.href = favicon.href.replace(`?${NOTIFICATION_PARAM}`, "");
favicon.href = favicon.href.replace("?notification", "");
}
},
};
+1 -1
View File
@@ -15,7 +15,7 @@ export async function downloadTrajectory(
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File", // This is a file type description, not user-facing text
description: "JSON File",
accept: {
"application/json": [".json"],
},
@@ -11,6 +11,6 @@ export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
.replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev")
.replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev")
.replace(/(^|\.)localhost$/, "auth.staging.all-hands.dev");
const scope = "openid email profile"; // OAuth scope - not user-facing
const scope = "openid email profile";
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
};
-1
View File
@@ -1,4 +1,3 @@
// These are provider names, not user-facing text
export const MAP_PROVIDER = {
openai: "OpenAI",
azure: "Azure",
+1 -3
View File
@@ -1,13 +1,11 @@
export type JupyterLine = { type: "plaintext" | "image"; content: string };
const IMAGE_PREFIX = "![image](data:image/png;base64,";
export const parseCellContent = (content: string) => {
const lines: JupyterLine[] = [];
let currentText = "";
for (const line of content.split("\n")) {
if (line.startsWith(IMAGE_PREFIX)) {
if (line.startsWith("![image](data:image/png;base64,")) {
if (currentText) {
lines.push({ type: "plaintext", content: currentText });
currentText = ""; // Reset after pushing plaintext
@@ -22,5 +22,5 @@ export const retrieveAxiosErrorMessage = (error: AxiosError) => {
errorMessage = error.message;
}
return errorMessage;
return errorMessage || "An error occurred";
};
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -51,7 +51,7 @@ When OpenHands works with a repository, it:
## Types of MicroAgents
Most microagents use markdown files with YAML frontmatter. For repository agents (repo.md), the frontmatter is optional - if not provided, the file will be loaded with default settings as a repository agent.
All microagents use markdown files with YAML frontmatter.
### 1. Knowledge Agents
@@ -147,7 +147,6 @@ You can see an example of a task-based agent in [OpenHands's pull request updati
- Specify testing and build procedures
- List environment requirements
- Maintain up-to-date team practices
- YAML frontmatter is optional - files without frontmatter will be loaded with default settings
### Submission Process
+1 -1
View File
@@ -14,7 +14,7 @@ the GitLab API.
You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
ALWAYS use the GitLab API for operations instead of a web browser.
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITLAB_TOKEN}@gitlab.com/username/repo.git`
Here are some instructions for pushing, but ONLY do this if the user asks you to:
* NEVER push directly to the `main` or `master` branch
+47 -11
View File
@@ -10,7 +10,6 @@ from litellm.exceptions import ( # noqa
APIError,
AuthenticationError,
BadRequestError,
ContentPolicyViolationError,
ContextWindowExceededError,
InternalServerError,
NotFoundError,
@@ -251,12 +250,7 @@ class AgentController:
self.state.last_error = err_id
elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e):
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
self.state.last_error = err_id
elif isinstance(e, ContentPolicyViolationError) or (
isinstance(e, BadRequestError)
and 'ContentPolicyViolationError' in str(e)
):
err_id = 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION'
# Set error reason for budget exceeded
self.state.last_error = err_id
elif isinstance(e, RateLimitError):
await self.set_agent_state_to(AgentState.RATE_LIMITED)
@@ -289,7 +283,6 @@ class AgentController:
or isinstance(e, InternalServerError)
or isinstance(e, AuthenticationError)
or isinstance(e, RateLimitError)
or isinstance(e, ContentPolicyViolationError)
or isinstance(e, LLMContextWindowExceedError)
):
reported = e
@@ -497,8 +490,15 @@ class AgentController:
if self.get_agent_state() != AgentState.RUNNING:
await self.set_agent_state_to(AgentState.RUNNING)
elif action.source == EventSource.AGENT:
# Check if we need to trigger microagents based on agent message content
recall_action = RecallAction(
query=action.content, recall_type=RecallType.KNOWLEDGE
)
self._pending_action = recall_action
# This is source=AGENT because the agent message is the trigger for the microagent retrieval
self.event_stream.add_event(recall_action, EventSource.AGENT)
# If the agent is waiting for a response, set the appropriate state
if action.wait_for_response:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
@@ -1084,8 +1084,44 @@ class AgentController:
# cut in half
mid_point = max(1, len(events) // 2)
kept_events = events[mid_point:]
if len(kept_events) > 0 and isinstance(kept_events[0], Observation):
kept_events = kept_events[1:]
# Handle first event in truncated history
if kept_events:
i = 0
while i < len(kept_events):
first_event = kept_events[i]
if isinstance(first_event, Observation) and first_event.cause:
# Find its action and include it
matching_action = next(
(
e
for e in reversed(events[:mid_point])
if isinstance(e, Action) and e.id == first_event.cause
),
None,
)
if matching_action:
kept_events = [matching_action] + kept_events
else:
self.log(
'warning',
f'Found Observation without matching Action at id={first_event.id}',
)
# drop this observation
kept_events = kept_events[1:]
break
elif isinstance(first_event, MessageAction) or (
isinstance(first_event, Action)
and first_event.source == EventSource.USER
):
# if it's a message action or a user action, keep it and continue to find the next event
i += 1
continue
else:
# if it's an action with source == EventSource.AGENT, we're good
break
# Ensure first user message is included
if first_user_msg and first_user_msg not in kept_events:
-33
View File
@@ -75,10 +75,6 @@ class LLMSummarizingCondenserConfig(BaseModel):
description='Maximum size of the condensed history before triggering forgetting.',
ge=2,
)
max_event_length: int = Field(
default=10_000,
description='Maximum length of the event representations to be passed to the LLM.',
)
model_config = {'extra': 'forbid'}
@@ -126,33 +122,6 @@ class LLMAttentionCondenserConfig(BaseModel):
model_config = {'extra': 'forbid'}
class StructuredSummaryCondenserConfig(BaseModel):
"""Configuration for StructuredSummaryCondenser instances."""
type: Literal['structured'] = Field('structured')
llm_config: LLMConfig = Field(
..., description='Configuration for the LLM to use for condensing.'
)
# at least one event by default, because the best guess is that it's the user task
keep_first: int = Field(
default=1,
description='Number of initial events to always keep in history.',
ge=0,
)
max_size: int = Field(
default=100,
description='Maximum size of the condensed history before triggering forgetting.',
ge=2,
)
max_event_length: int = Field(
default=10_000,
description='Maximum length of the event representations to be passed to the LLM.',
)
model_config = {'extra': 'forbid'}
# Type alias for convenience
CondenserConfig = (
NoOpCondenserConfig
@@ -162,7 +131,6 @@ CondenserConfig = (
| LLMSummarizingCondenserConfig
| AmortizedForgettingCondenserConfig
| LLMAttentionCondenserConfig
| StructuredSummaryCondenserConfig
)
@@ -265,7 +233,6 @@ def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
'llm': LLMSummarizingCondenserConfig,
'amortized': AmortizedForgettingCondenserConfig,
'llm_attention': LLMAttentionCondenserConfig,
'structured': StructuredSummaryCondenserConfig,
}
if condenser_type not in condenser_classes:
-10
View File
@@ -88,16 +88,6 @@ class LLMResponseError(Exception):
super().__init__(message)
# This exception should be retried
# Typically, after retry with a non-zero temperature, the LLM will return a response
class LLMNoResponseError(Exception):
def __init__(
self,
message: str = 'LLM did not return a response. This is only seen in Gemini models so far.',
) -> None:
super().__init__(message)
class UserCancelledError(Exception):
def __init__(self, message: str = 'User cancelled the request') -> None:
super().__init__(message)
+1 -5
View File
@@ -119,11 +119,7 @@ def initialize_repository_for_runtime(
if selected_repository and provider_tokens:
logger.debug(f'Selected repository {selected_repository}.')
repo_directory = call_async_from_sync(
runtime.clone_repo,
GENERAL_TIMEOUT,
provider_tokens,
selected_repository,
None,
runtime.clone_repo, GENERAL_TIMEOUT, github_token, selected_repository, None
)
# Run setup script if it exists
runtime.maybe_run_setup_script()
+21 -69
View File
@@ -7,37 +7,12 @@ from openhands.events.event import Event, EventSource
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.storage.files import FileStore
from openhands.storage.locations import (
get_conversation_dir,
get_conversation_event_filename,
get_conversation_events_dir,
)
from openhands.utils.shutdown_listener import should_continue
@dataclass(frozen=True)
class _CachePage:
events: list[dict] | None
start: int
end: int
def covers(self, global_index: int) -> bool:
if global_index < self.start:
return False
if global_index >= self.end:
return False
return True
def get_event(self, global_index: int) -> Event | None:
# If there was not actually a cached page, return None
if not self.events:
return None
local_index = global_index - self.start
return event_from_dict(self.events[local_index])
_DUMMY_PAGE = _CachePage(None, 1, -1)
@dataclass
class EventStore:
"""
@@ -48,7 +23,6 @@ class EventStore:
file_store: FileStore
user_id: str | None
cur_id: int = -1 # We fix this in post init if it is not specified
cache_size: int = 25
def __post_init__(self) -> None:
if self.cur_id >= 0:
@@ -109,33 +83,30 @@ class EventStore:
return True
return False
if end_id is None:
end_id = self.cur_id
else:
end_id += 1 # From inclusive to exclusive
if reverse:
step = -1
start_id, end_id = end_id, start_id
start_id -= 1
end_id -= 1
else:
step = 1
cache_page = _DUMMY_PAGE
for index in range(start_id, end_id, step):
if not should_continue():
return
if not cache_page.covers(index):
cache_page = self._load_cache_page_for_index(index)
event = cache_page.get_event(index)
if event is None:
if end_id is None:
end_id = self.cur_id - 1
event_id = end_id
while event_id >= start_id:
try:
event = self.get_event(index)
event = self.get_event(event_id)
if not should_filter(event):
yield event
except FileNotFoundError:
event = None
if event and not should_filter(event):
yield event
logger.debug(f'No event found for ID {event_id}')
event_id -= 1
else:
event_id = start_id
while should_continue():
if end_id is not None and event_id > end_id:
break
try:
event = self.get_event(event_id)
if not should_filter(event):
yield event
except FileNotFoundError:
break
event_id += 1
def get_event(self, id: int) -> Event:
filename = self._get_filename_for_id(id, self.user_id)
@@ -259,25 +230,6 @@ class EventStore:
def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
return get_conversation_event_filename(self.sid, id, user_id)
def _get_filename_for_cache(self, start: int, end: int) -> str:
return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
def _load_cache_page(self, start: int, end: int) -> _CachePage:
"""Read a page from the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
cache_filename = self._get_filename_for_cache(start, end)
try:
content = self.file_store.read(cache_filename)
events = json.loads(content)
except FileNotFoundError:
events = None
page = _CachePage(events, start, end)
return page
def _load_cache_page_for_index(self, index: int) -> _CachePage:
offset = index % self.cache_size
index -= offset
return self._load_cache_page(index, index + self.cache_size)
@staticmethod
def _get_id_from_filename(filename: str) -> int:
try:
-16
View File
@@ -52,7 +52,6 @@ class EventStream(EventStore):
_queue_loop: asyncio.AbstractEventLoop | None
_thread_pools: dict[str, dict[str, ThreadPoolExecutor]]
_thread_loops: dict[str, dict[str, asyncio.AbstractEventLoop]]
_write_page_cache: list[dict]
def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
super().__init__(sid, file_store, user_id)
@@ -67,7 +66,6 @@ class EventStream(EventStore):
self._subscribers = {}
self._lock = threading.Lock()
self.secrets = {}
self._write_page_cache = []
def _init_thread_loop(self, subscriber_id: str, callback_id: str) -> None:
loop = asyncio.new_event_loop()
@@ -173,22 +171,8 @@ class EventStream(EventStore):
self.file_store.write(
self._get_filename_for_id(event.id, self.user_id), json.dumps(data)
)
self._write_page_cache.append(data)
self._store_cache_page()
self._queue.put(event)
def _store_cache_page(self):
"""Store a page in the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
current_write_page = self._write_page_cache
if len(current_write_page) < self.cache_size:
return
self._write_page_cache = []
start = current_write_page[0]['id']
end = start + self.cache_size
contents = json.dumps(current_write_page)
cache_filename = self._get_filename_for_cache(start, end)
self.file_store.write(cache_filename, contents)
def set_secrets(self, secrets: dict[str, str]) -> None:
self.secrets = secrets.copy()
+30 -63
View File
@@ -16,7 +16,6 @@ from openhands.integrations.service_types import (
UnknownException,
User,
)
from openhands.server.types import AppMode
from openhands.utils.import_utils import get_impl
@@ -100,36 +99,31 @@ class GitHubService(GitService):
email=response.get('email'),
)
async def _fetch_paginated_repos(
self, url: str, params: dict, max_repos: int, extract_key: str | None = None
) -> list[dict]:
"""
Fetch repositories with pagination support.
Args:
url: The API endpoint URL
params: Query parameters for the request
max_repos: Maximum number of repositories to fetch
extract_key: If provided, extract repositories from this key in the response
Returns:
List of repository dictionaries
"""
repos: list[dict] = []
async def get_repositories(
self, sort: str, installation_id: int | None
) -> list[Repository]:
MAX_REPOS = 1000
PER_PAGE = 100 # Maximum allowed by GitHub API
all_repos: list[dict] = []
page = 1
while len(repos) < max_repos:
page_params = {**params, 'page': str(page)}
response, headers = await self._fetch_data(url, page_params)
while len(all_repos) < MAX_REPOS:
params = {'page': str(page), 'per_page': str(PER_PAGE)}
if installation_id:
url = (
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
)
response, headers = await self._fetch_data(url, params)
response = response.get('repositories', [])
else:
url = f'{self.BASE_URL}/user/repos'
params['sort'] = sort
response, headers = await self._fetch_data(url, params)
# Extract repositories from response
page_repos = response.get(extract_key, []) if extract_key else response
if not page_repos: # No more repositories
if not response: # No more repositories
break
repos.extend(page_repos)
all_repos.extend(response)
page += 1
# Check if we've reached the last page
@@ -137,43 +131,8 @@ class GitHubService(GitService):
if 'rel="next"' not in link_header:
break
return repos[:max_repos] # Trim to max_repos if needed
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
MAX_REPOS = 1000
PER_PAGE = 100 # Maximum allowed by GitHub API
all_repos: list[dict] = []
if app_mode == AppMode.SAAS:
# Get all installation IDs and fetch repos for each one
installation_ids = await self.get_installation_ids()
# Iterate through each installation ID
for installation_id in installation_ids:
params = {'per_page': str(PER_PAGE)}
url = (
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
)
# Fetch repositories for this installation
installation_repos = await self._fetch_paginated_repos(
url, params, MAX_REPOS - len(all_repos), extract_key='repositories'
)
all_repos.extend(installation_repos)
# If we've already reached MAX_REPOS, no need to check other installations
if len(all_repos) >= MAX_REPOS:
break
else:
# Original behavior for non-SaaS mode
params = {'per_page': str(PER_PAGE), 'sort': sort}
url = f'{self.BASE_URL}/user/repos'
# Fetch user repositories
all_repos = await self._fetch_paginated_repos(url, params, MAX_REPOS)
# Convert to Repository objects
# Trim to MAX_REPOS if needed and convert to Repository objects
all_repos = all_repos[:MAX_REPOS]
return [
Repository(
id=repo.get('id'),
@@ -359,6 +318,14 @@ class GitHubService(GitService):
except Exception:
return []
async def does_repo_exist(self, repository: str) -> bool:
url = f'{self.BASE_URL}/repos/{repository}'
try:
await self._fetch_data(url)
return True
except Exception:
return False
github_service_cls = os.environ.get(
'OPENHANDS_GITHUB_SERVICE_CLS',
@@ -13,7 +13,6 @@ from openhands.integrations.service_types import (
UnknownException,
User,
)
from openhands.server.types import AppMode
from openhands.utils.import_utils import get_impl
@@ -121,7 +120,12 @@ class GitLabService(GitService):
return repos
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
async def get_repositories(
self, sort: str, installation_id: int | None
) -> list[Repository]:
# if installation_id:
# return [] # Not implementing installation_token case yet
MAX_REPOS = 1000
PER_PAGE = 100 # Maximum allowed by GitLab API
all_repos: list[dict] = []
@@ -142,6 +146,7 @@ class GitLabService(GitService):
'per_page': str(PER_PAGE),
'order_by': order_by,
'sort': 'desc', # GitLab uses sort for direction (asc/desc)
'owned': 1, # Use 1 instead of True
'membership': 1, # Use 1 instead of True
}
response, headers = await self._fetch_data(url, params)
@@ -169,6 +174,16 @@ class GitLabService(GitService):
for repo in all_repos
]
async def does_repo_exist(self, repository: str) -> bool:
encoded_repo = quote_plus(repository)
url = f'{self.BASE_URL}/projects/{encoded_repo}'
try:
await self._fetch_data(url)
return True
except Exception as e:
print(e)
return False
gitlab_service_cls = os.environ.get(
'OPENHANDS_GITLAB_SERVICE_CLS',
+50 -4
View File
@@ -14,6 +14,7 @@ from pydantic import (
)
from pydantic.json import pydantic_encoder
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.action import Action
from openhands.events.action.commands import CmdRunAction
from openhands.events.stream import EventStream
@@ -26,7 +27,6 @@ from openhands.integrations.service_types import (
Repository,
User,
)
from openhands.server.types import AppMode
class ProviderToken(BaseModel):
@@ -188,7 +188,11 @@ class ProviderHandler:
service = self._get_service(provider)
return await service.get_latest_token()
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
async def get_repositories(
self,
sort: str,
installation_id: int | None,
) -> list[Repository]:
"""
Get repositories from a selected providers with pagination support
"""
@@ -197,7 +201,7 @@ class ProviderHandler:
for provider in self.provider_tokens:
try:
service = self._get_service(provider)
service_repos = await service.get_repositories(sort, app_mode)
service_repos = await service.get_repositories(sort, installation_id)
all_repos.extend(service_repos)
except Exception:
continue
@@ -224,6 +228,32 @@ class ProviderHandler:
return all_repos
async def get_remote_repository_url(self, repository: str) -> str | None:
if not repository:
return None
provider_domains = {
ProviderType.GITHUB: 'github.com',
ProviderType.GITLAB: 'gitlab.com',
}
for provider in self.provider_tokens:
try:
service = self._get_service(provider)
repo_exists = await service.does_repo_exist(repository)
if repo_exists:
git_token = self.provider_tokens[provider].token
if git_token and provider in provider_domains:
domain = provider_domains[provider]
if provider == ProviderType.GITLAB:
return f'https://oauth2:{git_token.get_secret_value()}@{domain}/{repository}.git'
return f'https://{git_token.get_secret_value()}@{domain}/{repository}.git'
except Exception:
continue
return None
async def set_event_stream_secrets(
self,
event_stream: EventStream,
@@ -289,7 +319,9 @@ class ProviderHandler:
get_latest: Get the latest working token for the providers if True, otherwise get the existing ones
"""
if not self.provider_tokens:
# TODO: We should remove `not get_latest` in the future. More
# details about the error this fixes is in the next comment below
if not self.provider_tokens and not get_latest:
return {}
env_vars: dict[ProviderType, SecretStr] = {}
@@ -310,6 +342,20 @@ class ProviderHandler:
if token:
env_vars[provider] = token
# TODO: we have an error where reinitializing the runtime doesn't happen with
# the provider tokens; thus the code above believes that github isn't a provider
# when it really is. We need to share information about current providers set
# for the user when the socket event for connect is sent
if ProviderType.GITHUB not in env_vars and get_latest:
logger.info(
f'Force refresh runtime token for user: {self.external_auth_id}'
)
service = GithubServiceImpl(
external_auth_id=self.external_auth_id,
external_token_manager=self.external_token_manager,
)
env_vars[ProviderType.GITHUB] = await service.get_latest_token()
if not expose_secrets:
return env_vars
+8 -3
View File
@@ -3,8 +3,6 @@ from typing import Protocol
from pydantic import BaseModel, SecretStr
from openhands.server.types import AppMode
class ProviderType(Enum):
GITHUB = 'github'
@@ -88,6 +86,13 @@ class GitService(Protocol):
"""Search for repositories"""
...
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
async def get_repositories(
self,
sort: str,
installation_id: int | None,
) -> list[Repository]:
"""Get repositories for the authenticated user"""
...
async def does_repo_exist(self, repository: str) -> bool:
"""Check if a repository exists for the user"""
+3 -21
View File
@@ -23,7 +23,6 @@ from litellm.exceptions import (
from litellm.types.utils import CostPerToken, ModelResponse, Usage
from litellm.utils import create_pretrained_tokenizer
from openhands.core.exceptions import LLMNoResponseError
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import Message
from openhands.llm.debug_mixin import DebugMixin
@@ -38,12 +37,7 @@ from openhands.llm.retry_mixin import RetryMixin
__all__ = ['LLM']
# tuple of exceptions to retry on
LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (
RateLimitError,
litellm.Timeout,
litellm.InternalServerError,
LLMNoResponseError,
)
LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (RateLimitError,)
# cache prompt supporting models
# remove this when we gemini and deepseek are supported
@@ -69,7 +63,6 @@ FUNCTION_CALLING_SUPPORTED_MODELS = [
'o1-2024-12-17',
'o3-mini-2025-01-31',
'o3-mini',
'gemini-2.5-pro',
]
REASONING_EFFORT_SUPPORTED_MODELS = [
@@ -274,12 +267,8 @@ class LLM(RetryMixin, DebugMixin):
# if we mocked function calling, and we have tools, convert the response back to function calling format
if mock_function_calling and mock_fncall_tools is not None:
if len(resp.choices) < 1:
raise LLMNoResponseError(
'Response choices is less than 1 - This is only seen in Gemini models so far. Response: '
+ str(resp)
)
logger.debug(f'Response choices: {len(resp.choices)}')
assert len(resp.choices) >= 1
non_fncall_response_message = resp.choices[0].message
fn_call_messages_with_response = (
convert_non_fncall_messages_to_fncall_messages(
@@ -293,13 +282,6 @@ class LLM(RetryMixin, DebugMixin):
)
resp.choices[0].message = fn_call_response_message
# Check if resp has 'choices' key with at least one item
if not resp.get('choices') or len(resp['choices']) < 1:
raise LLMNoResponseError(
'Response choices is less than 1 - This is only seen in Gemini models so far. Response: '
+ str(resp)
)
message_back: str = resp['choices'][0]['message']['content'] or ''
tool_calls: list[ChatCompletionMessageToolCall] = resp['choices'][0][
'message'
-17
View File
@@ -5,7 +5,6 @@ from tenacity import (
wait_exponential,
)
from openhands.core.exceptions import LLMNoResponseError
from openhands.core.logger import openhands_logger as logger
from openhands.utils.tenacity_stop import stop_if_should_exit
@@ -36,22 +35,6 @@ class RetryMixin:
if retry_listener:
retry_listener(retry_state.attempt_number, num_retries)
# Check if the exception is LLMNoResponseError
exception = retry_state.outcome.exception()
if isinstance(exception, LLMNoResponseError):
if hasattr(retry_state, 'kwargs'):
# Only change temperature if it's zero or not set
current_temp = retry_state.kwargs.get('temperature', 0)
if current_temp == 0:
retry_state.kwargs['temperature'] = 1.0
logger.warning(
'LLMNoResponseError detected with temperature=0, setting temperature to 1.0 for next attempt.'
)
else:
logger.warning(
f'LLMNoResponseError detected with temperature={current_temp}, keeping original temperature'
)
return retry(
before_sleep=before_sleep,
stop=stop_after_attempt(num_retries) | stop_if_should_exit(),
@@ -18,9 +18,6 @@ from openhands.memory.condenser.impl.observation_masking_condenser import (
from openhands.memory.condenser.impl.recent_events_condenser import (
RecentEventsCondenser,
)
from openhands.memory.condenser.impl.structured_summary_condenser import (
StructuredSummaryCondenser,
)
__all__ = [
'AmortizedForgettingCondenser',
@@ -31,5 +28,4 @@ __all__ = [
'ObservationMaskingCondenser',
'BrowserOutputCondenser',
'RecentEventsCondenser',
'StructuredSummaryCondenser',
]
@@ -4,7 +4,6 @@ from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
from openhands.core.message import Message, TextContent
from openhands.events.action.agent import CondensationAction
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.events.serialization.event import truncate_content
from openhands.llm import LLM
from openhands.memory.condenser.condenser import (
Condensation,
@@ -21,13 +20,7 @@ class LLMSummarizingCondenser(RollingCondenser):
and newly forgotten events.
"""
def __init__(
self,
llm: LLM,
max_size: int = 100,
keep_first: int = 1,
max_event_length: int = 10_000,
):
def __init__(self, llm: LLM, max_size: int = 100, keep_first: int = 1):
if keep_first >= max_size // 2:
raise ValueError(
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})'
@@ -39,15 +32,10 @@ class LLMSummarizingCondenser(RollingCondenser):
self.max_size = max_size
self.keep_first = keep_first
self.max_event_length = max_event_length
self.llm = llm
super().__init__()
def _truncate(self, content: str) -> str:
"""Truncate the content to fit within the specified maximum event length."""
return truncate_content(content, max_chars=self.max_event_length)
def get_condensation(self, view: View) -> Condensation:
head = view[: self.keep_first]
target_size = self.max_size // 2
@@ -68,66 +56,40 @@ class LLMSummarizingCondenser(RollingCondenser):
forgotten_events.append(event)
# Construct prompt for summarization
prompt = """You are maintaining a context-aware state summary for an interactive agent. You will be given a list of events corresponding to actions taken by the agent, and the most recent previous summary if one exists. Track:
prompt = """You are maintaining state history for an LLM-based code agent. Track:
USER_CONTEXT: (Preserve essential user requirements, goals, and clarifications in concise form)
USER_CONTEXT: (Preserve essential user requirements, problem descriptions, and clarifications in concise form)
COMPLETED: (Tasks completed so far, with brief results)
PENDING: (Tasks that still need to be done)
CURRENT_STATE: (Current variables, data structures, or relevant state)
For code-specific tasks, also include:
CODE_STATE: {File paths, function signatures, data structures}
STATE: {File paths, function signatures, data structures}
TESTS: {Failing cases, error messages, outputs}
CHANGES: {Code edits, variable updates}
DEPS: {Dependencies, imports, external calls}
VERSION_CONTROL_STATUS: {Repository state, current branch, PR status, commit history}
INTENT: {Why changes were made, acceptance criteria}
PRIORITIZE:
1. Adapt tracking format to match the actual task type
2. Capture key user requirements and goals
3. Distinguish between completed and pending tasks
4. Keep all sections concise and relevant
1. Capture key user requirements and constraints
2. Maintain critical problem context
3. Keep all sections concise
SKIP: Tracking irrelevant details for the current task type
SKIP: {Git clones, build logs, file listings}
Example formats:
Example history format:
USER_CONTEXT: Fix FITS card float representation - "0.009125" becomes "0.009124999999999999" causing comment truncation. Use Python's str() when possible while maintaining FITS compliance.
For code tasks:
USER_CONTEXT: Fix FITS card float representation issue
COMPLETED: Modified mod_float() in card.py, all tests passing
PENDING: Create PR, update documentation
CODE_STATE: mod_float() in card.py updated
STATE: mod_float() in card.py updated
TESTS: test_format() passed
CHANGES: str(val) replaces f"{val:.16G}"
DEPS: None modified
VERSION_CONTROL_STATUS: Branch: fix-float-precision, Latest commit: a1b2c3d
For other tasks:
USER_CONTEXT: Write 20 haikus based on coin flip results
COMPLETED: 15 haikus written for results [T,H,T,H,T,H,T,T,H,T,H,T,H,T,H]
PENDING: 5 more haikus needed
CURRENT_STATE: Last flip: Heads, Haiku count: 15/20"""
INTENT: Fix precision while maintaining FITS compliance"""
prompt += '\n\n'
# Add the previous summary if it exists. We'll always have a summary
# event, but the types aren't precise enought to guarantee that it has a
# message attribute.
summary_event_content = self._truncate(
summary_event.message if summary_event.message else ''
)
prompt += f'<PREVIOUS SUMMARY>\n{summary_event_content}\n</PREVIOUS SUMMARY>\n'
prompt += ('\n' + summary_event.message + '\n') if summary_event.message else ''
prompt += '\n\n'
# Add all events that are being forgotten. We use the string
# representation defined by the event, and truncate it if necessary.
for forgotten_event in forgotten_events:
event_content = self._truncate(str(forgotten_event))
prompt += f'<EVENT id={forgotten_event.id}>\n{event_content}\n</EVENT>\n'
prompt += 'Now summarize the events using the rules above.'
prompt += str(forgotten_event) + '\n\n'
messages = [Message(role='user', content=[TextContent(text=prompt)])]
@@ -159,7 +121,6 @@ CURRENT_STATE: Last flip: Heads, Haiku count: 15/20"""
llm=LLM(config=config.llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,
)
@@ -1,322 +0,0 @@
from __future__ import annotations
import json
from typing import Any
from pydantic import BaseModel, Field
from openhands.core.config.condenser_config import (
StructuredSummaryCondenserConfig,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import Message, TextContent
from openhands.events.action.agent import CondensationAction
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.events.serialization.event import truncate_content
from openhands.llm import LLM
from openhands.memory.condenser.condenser import (
Condensation,
RollingCondenser,
View,
)
class StateSummary(BaseModel):
"""A structured representation summarizing the state of the agent and the task."""
# Required core fields
user_context: str = Field(
default='',
description='Essential user requirements, goals, and clarifications in concise form.',
)
completed_tasks: str = Field(
default='', description='List of tasks completed so far with brief results.'
)
pending_tasks: str = Field(
default='', description='List of tasks that still need to be done.'
)
current_state: str = Field(
default='',
description='Current variables, data structures, or other relevant state information.',
)
# Code state fields
files_modified: str = Field(
default='', description='List of files that have been created or modified.'
)
function_changes: str = Field(
default='', description='List of functions that have been created or modified.'
)
data_structures: str = Field(
default='', description='List of key data structures in use or modified.'
)
# Test status fields
tests_written: str = Field(
default='',
description='Whether tests have been written for the changes. True, false, or unknown.',
)
tests_passing: str = Field(
default='',
description='Whether all tests are currently passing. True, false, or unknown.',
)
failing_tests: str = Field(
default='', description='List of names or descriptions of any failing tests.'
)
error_messages: str = Field(
default='', description='List of key error messages encountered.'
)
# Version control fields
branch_created: str = Field(
default='',
description='Whether a branch has been created for this work. True, false, or unknown.',
)
branch_name: str = Field(
default='', description='Name of the current working branch if known.'
)
commits_made: str = Field(
default='',
description='Whether any commits have been made. True, false, or unknown.',
)
pr_created: str = Field(
default='',
description='Whether a pull request has been created. True, false, or unknown.',
)
pr_status: str = Field(
default='',
description="Status of any pull request: 'draft', 'open', 'merged', 'closed', or 'unknown'.",
)
# Other fields
dependencies: str = Field(
default='',
description='List of dependencies or imports that have been added or modified.',
)
other_relevant_context: str = Field(
default='',
description="Any other important information that doesn't fit into the categories above.",
)
@classmethod
def tool_description(cls) -> dict[str, Any]:
"""Description of a tool whose arguments are the fields of this class.
Can be given to an LLM to force structured generation.
"""
properties = {}
# Build properties dictionary from field information
for field_name, field in cls.model_fields.items():
description = field.description or ''
properties[field_name] = {'type': 'string', 'description': description}
return {
'type': 'function',
'function': {
'name': 'create_state_summary',
'description': 'Creates a comprehensive summary of the current state of the interaction to preserve context when history grows too large. You must include non-empty values for user_context, completed_tasks, and pending_tasks.',
'parameters': {
'type': 'object',
'properties': properties,
'required': ['user_context', 'completed_tasks', 'pending_tasks'],
},
},
}
def __str__(self) -> str:
"""Format the state summary in a clear way for Claude 3.7 Sonnet."""
sections = [
'# State Summary',
'## Core Information',
f'**User Context**: {self.user_context}',
f'**Completed Tasks**: {self.completed_tasks}',
f'**Pending Tasks**: {self.pending_tasks}',
f'**Current State**: {self.current_state}',
'## Code Changes',
f'**Files Modified**: {self.files_modified}',
f'**Function Changes**: {self.function_changes}',
f'**Data Structures**: {self.data_structures}',
f'**Dependencies**: {self.dependencies}',
'## Testing Status',
f'**Tests Written**: {self.tests_written}',
f'**Tests Passing**: {self.tests_passing}',
f'**Failing Tests**: {self.failing_tests}',
f'**Error Messages**: {self.error_messages}',
'## Version Control',
f'**Branch Created**: {self.branch_created}',
f'**Branch Name**: {self.branch_name}',
f'**Commits Made**: {self.commits_made}',
f'**PR Created**: {self.pr_created}',
f'**PR Status**: {self.pr_status}',
'## Additional Context',
f'**Other Relevant Context**: {self.other_relevant_context}',
]
# Join all sections with double newlines
return '\n\n'.join(sections)
class StructuredSummaryCondenser(RollingCondenser):
"""A condenser that summarizes forgotten events.
Maintains a condensed history and forgets old events when it grows too large. Uses structured generation via function-calling to produce summaries that replace forgotten events.
"""
def __init__(
self,
llm: LLM,
max_size: int = 100,
keep_first: int = 1,
max_event_length: int = 10_000,
):
if keep_first >= max_size // 2:
raise ValueError(
f'keep_first ({keep_first}) must be less than half of max_size ({max_size})'
)
if keep_first < 0:
raise ValueError(f'keep_first ({keep_first}) cannot be negative')
if max_size < 1:
raise ValueError(f'max_size ({max_size}) cannot be non-positive')
if not llm.is_function_calling_active():
raise ValueError(
'LLM must support function calling to use StructuredSummaryCondenser'
)
self.max_size = max_size
self.keep_first = keep_first
self.max_event_length = max_event_length
self.llm = llm
super().__init__()
def _truncate(self, content: str) -> str:
"""Truncate the content to fit within the specified maximum event length."""
return truncate_content(content, max_chars=self.max_event_length)
def get_condensation(self, view: View) -> Condensation:
head = view[: self.keep_first]
target_size = self.max_size // 2
# Number of events to keep from the tail -- target size, minus however many
# prefix events from the head, minus one for the summarization event
events_from_tail = target_size - len(head) - 1
summary_event = (
view[self.keep_first]
if isinstance(view[self.keep_first], AgentCondensationObservation)
else AgentCondensationObservation('No events summarized')
)
# Identify events to be forgotten (those not in head or tail)
forgotten_events = []
for event in view[self.keep_first : -events_from_tail]:
if not isinstance(event, AgentCondensationObservation):
forgotten_events.append(event)
# Construct prompt for summarization
prompt = """You are maintaining a context-aware state summary for an interactive software agent. This summary is critical because it:
1. Preserves essential context when conversation history grows too large
2. Prevents lost work when the session length exceeds token limits
3. Helps maintain continuity across multiple interactions
You will be given:
- A list of events (actions taken by the agent)
- The most recent previous summary (if one exists)
Capture all relevant information, especially:
- User requirements that were explicitly stated
- Work that has been completed
- Tasks that remain pending
- Current state of code, variables, and data structures
- The status of any version control operations"""
prompt += '\n\n'
# Add the previous summary if it exists. We'll always have a summary
# event, but the types aren't precise enought to guarantee that it has a
# message attribute.
summary_event_content = self._truncate(
summary_event.message if summary_event.message else ''
)
prompt += f'<PREVIOUS SUMMARY>\n{summary_event_content}\n</PREVIOUS SUMMARY>\n'
prompt += '\n\n'
# Add all events that are being forgotten. We use the string
# representation defined by the event, and truncate it if necessary.
for forgotten_event in forgotten_events:
event_content = self._truncate(str(forgotten_event))
prompt += f'<EVENT id={forgotten_event.id}>\n{event_content}\n</EVENT>\n'
messages = [Message(role='user', content=[TextContent(text=prompt)])]
response = self.llm.completion(
messages=self.llm.format_messages_for_llm(messages),
tools=[StateSummary.tool_description()],
tool_choice={
'type': 'function',
'function': {'name': 'create_state_summary'},
},
)
try:
# Extract the message containing tool calls
message = response.choices[0].message
# Check if there are tool calls
if not hasattr(message, 'tool_calls') or not message.tool_calls:
raise ValueError('No tool calls found in response')
# Find the create_state_summary tool call
summary_tool_call = None
for tool_call in message.tool_calls:
if tool_call.function.name == 'create_state_summary':
summary_tool_call = tool_call
break
if not summary_tool_call:
raise ValueError('create_state_summary tool call not found')
# Parse the arguments
args_json = summary_tool_call.function.arguments
args_dict = json.loads(args_json)
# Create a StateSummary object
summary = StateSummary.model_validate(args_dict)
except (ValueError, AttributeError, KeyError, json.JSONDecodeError) as e:
logger.warning(
f'Failed to parse summary tool call: {e}. Using empty summary.'
)
summary = StateSummary()
self.add_metadata('response', response.model_dump())
self.add_metadata('metrics', self.llm.metrics.get())
return Condensation(
action=CondensationAction(
forgotten_events_start_id=min(event.id for event in forgotten_events),
forgotten_events_end_id=max(event.id for event in forgotten_events),
summary=str(summary),
summary_offset=self.keep_first,
)
)
def should_condense(self, view: View) -> bool:
return len(view) > self.max_size
@classmethod
def from_config(
cls, config: StructuredSummaryCondenserConfig
) -> StructuredSummaryCondenser:
return StructuredSummaryCondenser(
llm=LLM(config=config.llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,
)
StructuredSummaryCondenser.register_config(StructuredSummaryCondenserConfig)

Some files were not shown because too many files have changed in this diff Show More