Compare commits

...

28 Commits

Author SHA1 Message Date
openhands b53a52f5e9 Implement microagent suggestions using TipTap 2025-05-12 10:58:08 +00:00
Xingyao Wang da935f9d8f Merge branch 'main' into add-back-microagents 2025-05-03 00:04:17 +08:00
openhands 642cc52a1a Fix linting issues in handlers.ts 2025-05-02 13:06:21 +00:00
openhands 4c361ab9e5 Add mock handler for microagents endpoint 2025-05-02 09:23:25 +00:00
openhands 5dfa1bb6eb Fix microagent suggestions UI and TypeScript errors 2025-05-02 09:21:15 +00:00
Xingyao Wang a07cf972a5 Merge commit '6032d2620d6ec252d3c80695a6de1fc88da9c87a' into add-back-microagents 2025-05-02 09:03:17 +00:00
openhands f2e3bc3254 Fix microagent suggestions feature 2025-05-02 08:52:19 +00:00
openhands 3790ec7d60 Add tests for microagent suggestions component 2025-05-02 03:31:41 +00:00
openhands 3c0719309e Add microagent suggestions feature to chat input 2025-05-02 02:57:57 +00:00
Xingyao Wang 0236e0943e fix test 2025-05-02 02:09:27 +00:00
Xingyao Wang cd464c0022 rename files 2025-05-01 10:38:04 +08:00
Xingyao Wang 4519a7f4f3 fix test 2025-05-01 02:29:52 +00:00
Xingyao Wang fdc591330b add remain 2025-05-01 02:25:38 +00:00
Xingyao Wang 98e454e82c fix lint and missing imports 2025-05-01 02:25:24 +00:00
Xingyao Wang e088d2d24a simplify microagent 2025-05-01 02:13:46 +00:00
Xingyao Wang 58c574af1e revert changes 2025-05-01 02:13:00 +00:00
Xingyao Wang 405f0069f8 revert some changes 2025-05-01 02:03:06 +00:00
Xingyao Wang f26d770d03 remove hardcoded last line 2025-05-01 02:01:51 +00:00
Xingyao Wang bf2c3de219 cleanup tests 2025-04-30 11:11:23 +08:00
Xingyao Wang 7c35ce16e5 Merge branch 'main' into add-back-microagents 2025-04-30 11:07:17 +08:00
Xingyao Wang f4024ccd94 Update microagents/update_pr_description.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:43:37 +08:00
Xingyao Wang b55bfed831 Update microagents/address_pr_comments.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:38:22 +08:00
OpenHands Bot cb0994027f 🤖 Auto-fix Python linting issues 2025-04-29 16:02:04 +00:00
openhands bcc9bd0b9a Move task microagent tests to test_microagent_task.py 2025-04-29 02:12:14 +00:00
openhands 6c144e6b5a Add back microagent files with special handling for user inputs 2025-04-29 02:06:42 +00:00
openhands e90b841b0d Update microagent files to match original ones with added triggers and variable prompts 2025-04-29 01:48:10 +00:00
openhands a1e6ed4dff Add special handling for microagents that require user input 2025-04-29 01:47:18 +00:00
openhands ad6311d3cd Add back microagent files and add special handling for user input variables 2025-04-29 01:33:23 +00:00
19 changed files with 1948 additions and 102 deletions
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import { ChatInput } from "#/components/features/chat/chat-input";
import { describe, it, expect, vi } from "vitest";
// Mock TipTapEditor component
vi.mock("#/components/features/chat/tiptap-editor", () => ({
TipTapEditor: ({ value, onChange, onSubmit, placeholder, disabled, className }: any) => (
<div
data-testid="mock-tiptap-editor"
data-value={value}
data-disabled={disabled}
data-placeholder={placeholder}
className={className}
>
<button onClick={() => onChange("new value")}>Change</button>
<button onClick={() => onSubmit()}>Submit</button>
</div>
),
}));
describe("ChatInput with TipTap", () => {
it("renders the TipTap editor", () => {
const onSubmit = vi.fn();
render(
<ChatInput
onSubmit={onSubmit}
/>
);
expect(screen.getByTestId("mock-tiptap-editor")).toBeInTheDocument();
});
it("passes the correct props to TipTapEditor", () => {
const onSubmit = vi.fn();
const onChange = vi.fn();
render(
<ChatInput
value="Test value"
onSubmit={onSubmit}
onChange={onChange}
disabled={true}
className="test-class"
/>
);
const editor = screen.getByTestId("mock-tiptap-editor");
expect(editor).toHaveAttribute("data-value", "Test value");
expect(editor).toHaveAttribute("data-disabled", "true");
});
it("handles submit button click", () => {
const onSubmit = vi.fn();
render(
<ChatInput
value="Test value"
onSubmit={onSubmit}
/>
);
// Click the submit button
screen.getByText("Submit").click();
expect(onSubmit).toHaveBeenCalled();
});
});
@@ -0,0 +1,119 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { MicroagentSuggestions, MicroagentInfo } from "#/components/features/chat/microagent-suggestions";
import { describe, it, expect, vi, beforeEach } from "vitest";
describe("MicroagentSuggestions", () => {
const mockMicroagents: MicroagentInfo[] = [
{
name: "PR Update",
trigger: "/pr_update",
description: "Update a pull request",
},
{
name: "PR Comment",
trigger: "/pr_comment",
description: "Comment on a pull request",
},
{
name: "Test Update",
trigger: "/update_test",
description: "Update tests",
},
];
// Mock fetch
beforeEach(() => {
global.fetch = vi.fn().mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockMicroagents),
})
);
});
it("should render microagent suggestions when visible", async () => {
const onSelect = vi.fn();
render(
<MicroagentSuggestions
query="/"
isVisible={true}
onSelect={onSelect}
/>
);
// Wait for the fetch to complete
await vi.waitFor(() => {
expect(screen.getByText("/pr_update")).toBeInTheDocument();
});
expect(screen.getByText("/pr_comment")).toBeInTheDocument();
expect(screen.getByText("/update_test")).toBeInTheDocument();
});
it("should filter microagents based on query", async () => {
const onSelect = vi.fn();
const { rerender } = render(
<MicroagentSuggestions
query="/"
isVisible={true}
onSelect={onSelect}
/>
);
// Wait for the fetch to complete
await vi.waitFor(() => {
expect(screen.getByText("/pr_update")).toBeInTheDocument();
});
// Rerender with a filtered query
rerender(
<MicroagentSuggestions
query="/pr"
isVisible={true}
onSelect={onSelect}
/>
);
expect(screen.getByText("/pr_update")).toBeInTheDocument();
expect(screen.getByText("/pr_comment")).toBeInTheDocument();
expect(screen.queryByText("/update_test")).not.toBeInTheDocument();
});
it("should call onSelect when a microagent is clicked", async () => {
const onSelect = vi.fn();
render(
<MicroagentSuggestions
query="/"
isVisible={true}
onSelect={onSelect}
/>
);
// Wait for the fetch to complete
await vi.waitFor(() => {
expect(screen.getByText("/pr_update")).toBeInTheDocument();
});
// Click on a microagent
fireEvent.click(screen.getByText("/pr_update"));
expect(onSelect).toHaveBeenCalledWith("/pr_update");
});
it("should not render when isVisible is false", () => {
const onSelect = vi.fn();
render(
<MicroagentSuggestions
query="/"
isVisible={false}
onSelect={onSelect}
/>
);
expect(screen.queryByText("Loading microagents...")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,71 @@
import { render, screen, within } from "@testing-library/react";
import { TipTapEditor } from "#/components/features/chat/tiptap-editor";
import { describe, it, expect, vi } from "vitest";
// Add a custom query to find elements by attribute
screen.getByAttribute = (attribute: string, value: string) => {
return document.querySelector(`[${attribute}="${value}"]`);
};
// Mock fetch for microagents
global.fetch = vi.fn().mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([
{
name: "PR Update",
trigger: "/pr_update",
description: "Update a pull request",
},
{
name: "PR Comment",
trigger: "/pr_comment",
description: "Comment on a pull request",
},
]),
})
);
describe("TipTapEditor", () => {
it("renders the editor", () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
render(
<TipTapEditor
value=""
onChange={onChange}
onSubmit={onSubmit}
placeholder="Test placeholder"
/>
);
// Check that the editor is rendered
const editorElement = screen.getByAttribute("contenteditable", "true");
expect(editorElement).toBeInTheDocument();
});
it("passes the correct props", () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
const onFocus = vi.fn();
const onBlur = vi.fn();
render(
<TipTapEditor
value="Test value"
onChange={onChange}
onSubmit={onSubmit}
onFocus={onFocus}
onBlur={onBlur}
placeholder="Test placeholder"
disabled={true}
className="test-class"
/>
);
// Check that the editor has the correct class
const editorElement = screen.getByAttribute("contenteditable", "true");
expect(editorElement?.classList.contains("test-class")).toBeTruthy();
});
});
+759 -3
View File
@@ -18,6 +18,11 @@
"@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.0",
"@tanstack/react-query": "^5.75.1",
"@tiptap/extension-mention": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -3591,6 +3596,16 @@
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"license": "MIT"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/breadcrumbs": {
"version": "3.5.23",
"resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.23.tgz",
@@ -5156,6 +5171,12 @@
}
}
},
"node_modules/@remirror/core-constants": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
"license": "MIT"
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
@@ -5937,6 +5958,435 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tiptap/core": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.12.0.tgz",
"integrity": "sha512-3qX8oGVKFFZzQ0vit+ZolR6AJIATBzmEmjAA0llFhWk4vf3v64p1YcXcJsOBsr5scizJu5L6RYWEFatFwqckRg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.12.0.tgz",
"integrity": "sha512-XUC2A77YAPMJS2SqZ2S62IGcUH8gZ7cdhoWlYQb1pR4ZzXFByeKDJPxfYeAePSiuI01YGrlzgY2c6Ncx/DtO0A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.12.0.tgz",
"integrity": "sha512-lAUtoLDLRc5ofD2I9MFY6MQ7d1qBLLqS1rvpwaPjOaoQb/GPVnaHj9qXYG0SY9K3erMtto48bMFpAcscjZHzZQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.12.0.tgz",
"integrity": "sha512-DYijoE0igV0Oi+ZppFsp2UrQsM/4HZtmmpD78BJM9zfCbd1YvAUIxmzmXr8uqU18OHd1uQy+/zvuNoUNYyw67g==",
"license": "MIT",
"dependencies": {
"tippy.js": "^6.3.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.12.0.tgz",
"integrity": "sha512-YTCjztB8MaIpwyxFYr81H4+LdKCq1VlaSXQyrPdB44mVdhhRqc46BYQb8/B//XE3UIu3X2QWFjwrqRlUq6vUiw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-code": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.12.0.tgz",
"integrity": "sha512-R7RaS+hJeHFim7alImQ9L9CSWSMjWXvz0Ote568x9ea5gdBGUYW8PcH+5a91lh8e1XGYWBM12a8oJZRyxg/tQA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.12.0.tgz",
"integrity": "sha512-1D7cYAjgxEFHdfC/35Ooi4GqWKB5sszbW8iI7N16XILNln26xb0d5KflXqYrwr9CN/ZnZoCl2o6YsP7xEObcZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-document": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.12.0.tgz",
"integrity": "sha512-sA1Q+mxDIv0Y3qQTBkYGwknNbDcGFiJ/fyAFholXpqbrcRx3GavwR/o0chBdsJZlFht0x7AWGwUYWvIo7wYilA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.12.0.tgz",
"integrity": "sha512-zcZSOXFj+7LVnmdPWTfKr5AoxYIzFPFlLJe35AdTQC5IhkljLn1Exct8I30ZREojX/00hKYsO7JJmePS6TEVlQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-floating-menu": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.12.0.tgz",
"integrity": "sha512-BYpyZx/56KCDksWuJJbhki/uNgt9sACuSSZFH5AN1yS1ISD+EzIxqf6Pzzv8QCoNJ+KcRNVaZsOlOFaJGoyzag==",
"license": "MIT",
"dependencies": {
"tippy.js": "^6.3.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.12.0.tgz",
"integrity": "sha512-k8ji5v9YKn7bNjo8UtI9hEfXfl4tKUp1hpJOEmUxGJQa3LIwrwSbReupUTnHszGQelzxikS/l1xO9P0TIGwRoA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.12.0.tgz",
"integrity": "sha512-08MNS2PK5DzdnAfqXn4krmJ/xebKmWpRpYqqN5EM8AvetYKlAJyTVSpo0ZUeGbZ3EZiPm9djgSnrLqpFUDjRCg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.12.0.tgz",
"integrity": "sha512-9DfES4Wd5TX1foI70N9sAL+35NN1UHrtzDYN2+dTHupnmKir9RaMXyZcbkUb4aDVzYrGxIqxJzHBVkquKIlTrw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-history": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.12.0.tgz",
"integrity": "sha512-+B9CAf2BFURC6mQiM1OQtahVTzdEOEgT/UUNlRZkeeBc0K5of3dr6UdBqaoaMAefja3jx5PqiQ7mhUBAjSt6AA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.12.0.tgz",
"integrity": "sha512-Vi2+6RIehDSpoJn/7PDuOieUj7W7WrEb4wBxK9TG8PDscihR0mehhhzm/K2xhH4TN48iPJGRsjDFrFjTbXmcnw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.12.0.tgz",
"integrity": "sha512-JKcXK3LmEsmxNzEq5e06rPUGMRLUxmJ2mYtBY4NlJ6yLM9XMDljtgeTnWT0ySLYmfINSFTkX4S7WIRbpl9l4pw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.12.0.tgz",
"integrity": "sha512-4YwZooC8HP+gPxs6YrkB1ayggyYbgVvJx/rWBT6lKSW2MVVg8QXi1zAcSI3MhIhHmqDysXXFPL8JURlbeGjaFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-mention": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.12.0.tgz",
"integrity": "sha512-+b/fqOU+pRWWAo0ZfyInkhkvV0Ub5RpNrYZ45v2nn5PjbXbxyxNQ51zT6cGk2F6Jmc6UBmlR8iqqNTIQY9ieEg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0",
"@tiptap/suggestion": "^2.7.0"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.12.0.tgz",
"integrity": "sha512-1ys0e/oqk09oXxrB1WzAx5EntK/QreObG/V1yhgihGm429fxHMsxzIYN6dKAYxx0YOPQG7qEZRrrPuWU70Ms7g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.12.0.tgz",
"integrity": "sha512-QNK5cgewCunWFxpLlbvvoO1rrLgEtNKxiY79fctP9toV+e59R+1i1Q9lXC1O5mOfDgVxCb6uFDMsqmKhFjpPog==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-placeholder": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.12.0.tgz",
"integrity": "sha512-K7irDox4P+NLAMjVrJeG72f0sulsCRYpx1Cy4gxKCdi1LTivj5VkXa6MXmi42KTCwBu3pWajBctYIOAES1FTAA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.12.0.tgz",
"integrity": "sha512-nBaa5YtBsLJPZFfSs36sBz4Zgi/c8b3MsmS/Az8uXaHb0R9yPewOVUMDIQbxMct8SXUlIo9VtKlOL+mVJ3Nkpw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-text": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.12.0.tgz",
"integrity": "sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/extension-text-style": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.12.0.tgz",
"integrity": "sha512-Pxwt23ZlvbQUahV0PvHy8Ej6IAuKR1FvHobUvwP3T8AiY7hob66fWRe7tQbESzSAzm5Vv2xkvyHeU8vekMTezA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0"
}
},
"node_modules/@tiptap/pm": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.12.0.tgz",
"integrity": "sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.23.0",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.37.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/react": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.12.0.tgz",
"integrity": "sha512-D+PR+4kJO9h8AB/7XyQ/Anw8tqeS2ecv5QemBOCHi9JlMAjytauUrj6IfFBO9RbsCowlBjW5GnSpFhzpk2Gghg==",
"license": "MIT",
"dependencies": {
"@tiptap/extension-bubble-menu": "^2.12.0",
"@tiptap/extension-floating-menu": "^2.12.0",
"@types/use-sync-external-store": "^0.0.6",
"fast-deep-equal": "^3",
"use-sync-external-store": "^1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.12.0.tgz",
"integrity": "sha512-wlcEEtexd6u0gbR311/OFZnbtRWU97DUsY6/GsSQzN4rqZ7Ra6YbfHEN5Lutu+I/anomK8vKy8k9NyvfY5Hllg==",
"license": "MIT",
"dependencies": {
"@tiptap/core": "^2.12.0",
"@tiptap/extension-blockquote": "^2.12.0",
"@tiptap/extension-bold": "^2.12.0",
"@tiptap/extension-bullet-list": "^2.12.0",
"@tiptap/extension-code": "^2.12.0",
"@tiptap/extension-code-block": "^2.12.0",
"@tiptap/extension-document": "^2.12.0",
"@tiptap/extension-dropcursor": "^2.12.0",
"@tiptap/extension-gapcursor": "^2.12.0",
"@tiptap/extension-hard-break": "^2.12.0",
"@tiptap/extension-heading": "^2.12.0",
"@tiptap/extension-history": "^2.12.0",
"@tiptap/extension-horizontal-rule": "^2.12.0",
"@tiptap/extension-italic": "^2.12.0",
"@tiptap/extension-list-item": "^2.12.0",
"@tiptap/extension-ordered-list": "^2.12.0",
"@tiptap/extension-paragraph": "^2.12.0",
"@tiptap/extension-strike": "^2.12.0",
"@tiptap/extension-text": "^2.12.0",
"@tiptap/extension-text-style": "^2.12.0",
"@tiptap/pm": "^2.12.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/suggestion": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.12.0.tgz",
"integrity": "sha512-bsXLoZbjUo1oOF1Z+XSfoGzbcnrTcYtJdfylM/FerMLU9T12dhsM/Ri2SKLX4IR5D0HJ07FcsEHCrGEy8Y5y0A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.7.0",
"@tiptap/pm": "^2.7.0"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -6038,6 +6488,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
@@ -6053,6 +6509,16 @@
"@types/lodash": "*"
}
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -6062,6 +6528,12 @@
"@types/unist": "*"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -6856,7 +7328,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
@@ -7964,6 +8435,12 @@
}
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -8747,7 +9224,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -9554,7 +10030,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@@ -11580,6 +12055,15 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/lint-staged": {
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz",
@@ -12040,6 +12524,35 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@@ -12341,6 +12854,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -13647,6 +14166,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
"license": "MIT"
},
"node_modules/outvariant": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
@@ -14308,6 +14833,201 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/prosemirror-changeset": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.0.tgz",
"integrity": "sha512-8wRKhlEwEJ4I13Ju54q2NZR1pVKGTgJ/8XsQ8L5A5uUsQ/YQScQJuEAuh8Bn8i6IwAMjjLRABd9lVli+DlIiVw==",
"license": "MIT",
"dependencies": {
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-collab": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-commands": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-dropcursor": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz",
"integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"node_modules/prosemirror-history": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz",
"integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-inputrules": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz",
"integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-markdown": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
"integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
"license": "MIT",
"dependencies": {
"@types/markdown-it": "^14.0.0",
"markdown-it": "^14.0.0",
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-menu": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
"integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
"license": "MIT",
"dependencies": {
"crelt": "^1.0.0",
"prosemirror-commands": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz",
"integrity": "sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-schema-basic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz",
"integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.25.0",
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.10.3",
"prosemirror-view": "^1.39.1"
}
},
"node_modules/prosemirror-trailing-node": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
"license": "MIT",
"dependencies": {
"@remirror/core-constants": "3.0.0",
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
"prosemirror-model": "^1.22.1",
"prosemirror-state": "^1.4.2",
"prosemirror-view": "^1.33.8"
}
},
"node_modules/prosemirror-transform": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz",
"integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.39.2",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.39.2.tgz",
"integrity": "sha512-BmOkml0QWNob165gyUxXi5K5CVUgVPpqMEAAml/qzgKn9boLUWVPzQ6LtzXw8Cn1GtRQX4ELumPxqtLTDaAKtg==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -14350,6 +15070,15 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -15170,6 +15899,12 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
"license": "MIT"
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
@@ -16536,6 +17271,15 @@
"node": ">=14.0.0"
}
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
@@ -16841,6 +17585,12 @@
"node": ">=14.17"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -17457,6 +18207,12 @@
"node": ">=0.10.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+5
View File
@@ -17,6 +17,11 @@
"@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.0",
"@tanstack/react-query": "^5.75.1",
"@tiptap/extension-mention": "^2.12.0",
"@tiptap/extension-placeholder": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -1,10 +1,10 @@
import React from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { SubmitButton } from "#/components/shared/buttons/submit-button";
import { StopButton } from "#/components/shared/buttons/stop-button";
import { TipTapEditor } from "./tiptap-editor";
interface ChatInputProps {
name?: string;
@@ -28,7 +28,7 @@ export function ChatInput({
button = "submit",
disabled,
showButton = true,
value,
value = "",
maxRows = 16,
onSubmit,
onStop,
@@ -40,105 +40,35 @@ export function ChatInput({
buttonClassName,
}: ChatInputProps) {
const { t } = useTranslation();
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value);
const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
// Only handle paste if we have an image paste handler and there are files
if (onImagePaste && event.clipboardData.files.length > 0) {
const files = Array.from(event.clipboardData.files).filter((file) =>
file.type.startsWith("image/"),
);
// Only prevent default if we found image files to handle
if (files.length > 0) {
event.preventDefault();
onImagePaste(files);
}
}
// For text paste, let the default behavior handle it
};
const handleDragOver = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
if (event.dataTransfer.types.includes("Files")) {
setIsDraggingOver(true);
}
};
const handleDragLeave = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
setIsDraggingOver(false);
};
const handleDrop = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
setIsDraggingOver(false);
if (onImagePaste && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith("image/"),
);
if (files.length > 0) {
onImagePaste(files);
}
}
const handleChange = (newValue: string) => {
setInputValue(newValue);
onChange?.(newValue);
};
const handleSubmitMessage = () => {
const message = value || textareaRef.current?.value || "";
if (message.trim()) {
onSubmit(message);
if (inputValue.trim()) {
onSubmit(inputValue);
setInputValue("");
onChange?.("");
if (textareaRef.current) {
textareaRef.current.value = "";
}
}
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!disabled &&
!event.nativeEvent.isComposing
) {
event.preventDefault();
handleSubmitMessage();
}
};
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange?.(event.target.value);
};
return (
<div
data-testid="chat-input"
className="flex items-end justify-end grow gap-1 min-h-6 w-full"
className="flex items-end justify-end grow gap-1 min-h-6 w-full relative"
>
<TextareaAutosize
ref={textareaRef}
name={name}
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
onKeyDown={handleKeyPress}
<TipTapEditor
value={inputValue}
onChange={handleChange}
onSubmit={handleSubmitMessage}
onFocus={onFocus}
onBlur={onBlur}
onPaste={handlePaste}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
value={value}
minRows={1}
maxRows={maxRows}
data-dragging-over={isDraggingOver}
className={cn(
"grow text-sm self-center placeholder:text-neutral-400 text-white resize-none outline-none ring-0",
"transition-all duration-200 ease-in-out",
isDraggingOver
? "bg-neutral-600/50 rounded-lg px-2"
: "bg-transparent",
className,
)}
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
disabled={disabled}
className={className}
/>
{showButton && (
<div className={buttonClassName}>
@@ -0,0 +1,106 @@
import React, { useEffect, useState } from "react";
import { cn } from "#/utils/utils";
export interface MicroagentInfo {
name: string;
trigger: string;
description: string;
}
interface MicroagentSuggestionsProps {
query: string;
isVisible: boolean;
onSelect: (trigger: string) => void;
className?: string;
}
export function MicroagentSuggestions({
query,
isVisible,
onSelect,
className,
}: MicroagentSuggestionsProps) {
const [microagents, setMicroagents] = useState<MicroagentInfo[]>([]);
const [filteredMicroagents, setFilteredMicroagents] = useState<
MicroagentInfo[]
>([]);
const [loading, setLoading] = useState(false);
// Fetch microagents when the component mounts
useEffect(() => {
const fetchMicroagents = async () => {
try {
setLoading(true);
const response = await fetch("/api/options/microagents");
if (response.ok) {
const data = await response.json();
setMicroagents(data);
}
} catch (error) {
// Log error silently
} finally {
setLoading(false);
}
};
fetchMicroagents();
}, []);
// Filter microagents based on the query
useEffect(() => {
if (!query || query === "/") {
setFilteredMicroagents(microagents);
} else {
const searchTerm = query.slice(1).toLowerCase();
const filtered = microagents.filter(
(agent) =>
agent.trigger.toLowerCase().includes(searchTerm) ||
agent.name.toLowerCase().includes(searchTerm),
);
setFilteredMicroagents(filtered);
}
}, [query, microagents]);
if (!isVisible || (filteredMicroagents.length === 0 && !loading)) {
return null;
}
return (
<div
className={cn(
"absolute bottom-full left-0 w-full max-h-60 overflow-y-auto bg-neutral-800 rounded-md shadow-lg z-10 mb-1 border border-neutral-600",
className,
)}
>
{loading && (
<div className="p-2 text-neutral-400">Loading microagents...</div>
)}
{!loading && filteredMicroagents.length === 0 && (
<div className="p-2 text-neutral-400">No microagents found</div>
)}
{!loading && filteredMicroagents.length > 0 && (
<ul className="py-1">
{filteredMicroagents.map((agent) => (
<div
key={agent.trigger}
className="px-3 py-2 hover:bg-neutral-700 cursor-pointer flex flex-col"
onClick={() => onSelect(agent.trigger)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onSelect(agent.trigger);
}
}}
>
<span className="font-medium text-white">{agent.trigger}</span>
<span className="text-xs text-neutral-400 truncate">
{agent.description}
</span>
</div>
))}
</ul>
)}
</div>
);
}
@@ -0,0 +1,57 @@
.ProseMirror {
padding: 0.5rem;
min-height: 2rem;
outline: none;
width: 100%;
font-size: 0.875rem;
line-height: 1.25rem;
color: white;
background-color: transparent;
transition: all 0.2s ease-in-out;
}
.ProseMirror p {
margin: 0;
}
.ProseMirror:focus {
outline: none;
box-shadow: none;
}
.microagent-mention {
color: #3b82f6;
font-weight: 500;
}
.microagent-suggestions-popup {
z-index: 50;
}
/* Placeholder styling */
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #9ca3af;
pointer-events: none;
height: 0;
}
/* Dropdown styling */
.microagent-suggestions-popup .bg-neutral-800 {
background-color: #262626;
border-color: #525252;
}
.microagent-suggestions-popup .hover\:bg-neutral-700:hover,
.microagent-suggestions-popup .bg-neutral-700 {
background-color: #404040;
}
.microagent-suggestions-popup .text-white {
color: white;
}
.microagent-suggestions-popup .text-neutral-400 {
color: #a3a3a3;
}
@@ -0,0 +1,250 @@
import React, { useEffect, useCallback, useState } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Mention } from '@tiptap/extension-mention';
import Placeholder from '@tiptap/extension-placeholder';
import { createRoot } from 'react-dom/client';
import { cn } from "#/utils/utils";
import './tiptap-editor.css';
export interface MicroagentInfo {
name: string;
trigger: string;
description: string;
}
interface TipTapEditorProps {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
export function TipTapEditor({
value,
onChange,
onSubmit,
onFocus,
onBlur,
placeholder = 'What would you like to build?',
disabled = false,
className,
}: TipTapEditorProps) {
const [microagents, setMicroagents] = useState<MicroagentInfo[]>([]);
const [loading, setLoading] = useState(false);
// Fetch microagents when the component mounts
useEffect(() => {
const fetchMicroagents = async () => {
try {
setLoading(true);
const response = await fetch("/api/options/microagents");
if (response.ok) {
const data = await response.json();
setMicroagents(data);
}
} catch (error) {
console.error('Error fetching microagents:', error);
} finally {
setLoading(false);
}
};
fetchMicroagents();
}, []);
// Custom suggestion handler for microagents
const suggestionHandler = useCallback(() => {
return {
char: '/',
items: ({ query }: { query: string }) => {
if (!query) return microagents;
return microagents.filter(item =>
item.trigger.toLowerCase().includes(query.toLowerCase()) ||
item.name.toLowerCase().includes(query.toLowerCase())
);
},
render: () => {
let popup: HTMLElement | null = null;
let component: React.ReactNode | null = null;
return {
onStart: (props: any) => {
popup = document.createElement('div');
popup.classList.add('microagent-suggestions-popup');
document.body.appendChild(popup);
component = (
<div className="absolute z-50 bg-neutral-800 rounded-md shadow-lg border border-neutral-600 w-64 max-h-60 overflow-y-auto">
{loading ? (
<div className="p-2 text-neutral-400">Loading microagents...</div>
) : props.items.length === 0 ? (
<div className="p-2 text-neutral-400">No microagents found</div>
) : (
<ul className="py-1">
{props.items.map((item: MicroagentInfo, index: number) => (
<div
key={item.trigger}
className={cn(
"px-3 py-2 hover:bg-neutral-700 cursor-pointer flex flex-col",
index === props.selectedIndex ? "bg-neutral-700" : ""
)}
onClick={() => props.command(item)}
>
<span className="font-medium text-white">{item.trigger}</span>
<span className="text-xs text-neutral-400 truncate">
{item.description}
</span>
</div>
))}
</ul>
)}
</div>
);
if (popup) {
const { view, clientRect } = props;
const { top, left } = clientRect();
// Position the popup
popup.style.position = 'absolute';
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
// Render the component into the popup
const root = createRoot(popup);
root.render(component);
}
},
onUpdate: (props: any) => {
if (popup) {
const { clientRect } = props;
const { top, left } = clientRect();
// Update position
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
// Re-render with updated props
const root = createRoot(popup);
root.render(component);
}
},
onKeyDown: (props: any) => {
if (props.event.key === 'Escape') {
props.event.preventDefault();
return true;
}
return false;
},
onExit: () => {
if (popup) {
document.body.removeChild(popup);
popup = null;
component = null;
}
},
};
},
command: ({ editor, range, props }: any) => {
// Insert the selected microagent trigger
editor
.chain()
.focus()
.deleteRange(range)
.insertContent(props.trigger.replace('/', '') + ' ')
.run();
},
};
}, [microagents, loading]);
const editor = useEditor({
extensions: [
StarterKit,
Placeholder.configure({
placeholder,
emptyEditorClass: 'is-editor-empty',
}),
Mention.configure({
HTMLAttributes: {
class: 'microagent-mention',
},
suggestion: suggestionHandler(),
renderLabel: ({ node }) => node.attrs.label,
}),
],
content: value,
onUpdate: ({ editor }) => {
onChange(editor.getText());
},
editorProps: {
attributes: {
class: cn(
'prose prose-sm focus:outline-none w-full max-w-full',
'text-white placeholder:text-neutral-400',
className
),
},
handleKeyDown: (view, event) => {
// Handle Enter key for submission
if (event.key === 'Enter' && !event.shiftKey && !disabled) {
event.preventDefault();
const text = view.state.doc.textContent;
if (text.trim()) {
onSubmit(text);
// Clear the editor
view.dispatch(view.state.tr.delete(0, view.state.doc.content.size));
}
return true;
}
return false;
},
},
});
// Update editor content when value prop changes
useEffect(() => {
if (editor && editor.getText() !== value) {
editor.commands.setContent(value);
}
}, [value, editor]);
// Handle focus and blur events
useEffect(() => {
if (!editor) return;
const handleFocus = () => {
if (onFocus) onFocus();
};
const handleBlur = () => {
if (onBlur) onBlur();
};
editor.on('focus', handleFocus);
editor.on('blur', handleBlur);
return () => {
editor.off('focus', handleFocus);
editor.off('blur', handleBlur);
};
}, [editor, onFocus, onBlur]);
return (
<EditorContent
editor={editor}
className={cn(
"grow text-sm self-center resize-none outline-none ring-0",
"transition-all duration-200 ease-in-out",
"bg-transparent",
className
)}
/>
);
}
+20
View File
@@ -103,6 +103,26 @@ const openHandsHandlers = [
HttpResponse.json(["mock-invariant"]),
),
http.get("/api/options/microagents", async () =>
HttpResponse.json([
{
name: "PR Update",
trigger: "/pr_update",
description: "Update a pull request",
},
{
name: "PR Comment",
trigger: "/pr_comment",
description: "Comment on a pull request",
},
{
name: "Test Update",
trigger: "/update_test",
description: "Update tests",
},
]),
),
http.post("http://localhost:3001/api/submit-feedback", async () => {
await delay(1200);
+66
View File
@@ -0,0 +1,66 @@
---
name: add_repo_inst
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /add_repo_inst
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
Specifically, I want you to create a `.openhands/microagents/repo.md` file. This file should contain succinct information that summarizes (1) the purpose of this repository, (2) the general setup of this repo, and (3) a brief description of the structure of this repo.
Here's an example:
```markdown
---
name: repo
type: repo
agent: CodeActAgent
---
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
(in the `openhands` directory) and React frontend (in the `frontend` directory).
## General Setup:
To set up the entire repo, including frontend and backend, run `make build`.
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
Before pushing any changes, you should ensure that any lint errors or simple test errors have been fixed.
* If you've made changes to the backend, you should run `pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
If either command fails, it may have automatically fixed some issues. You should fix any issues that weren't automatically fixed,
then re-run the command to ensure it passes.
## Repository Structure
Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
Frontend:
- Located in the `frontend` directory
- Prerequisites: A recent version of NodeJS / NPM
- Setup: Run `npm install` in the frontend directory
- Testing:
- Run tests: `npm run test`
- To run specific tests: `npm run test -- -t "TestName"`
- Building:
- Build for production: `npm run build`
- Environment Variables:
- Set in `frontend/.env` or as environment variables
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
- Internationalization:
- Generate i18n declaration file: `npm run make-i18n`
```
Now, please write a similar markdown for the current repository.
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
+26
View File
@@ -0,0 +1,26 @@
---
name: get_test_to_pass
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
{%- endif %}
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
+19
View File
@@ -0,0 +1,19 @@
---
name: pr_comments
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /pr_comments
inputs:
- name: PR_URL
description: "URL of the pull request"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
This branch corresponds to this PR {{ PR_URL }}
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.
+22
View File
@@ -0,0 +1,22 @@
---
name: pr_update
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /pr_update
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.
+22
View File
@@ -0,0 +1,22 @@
---
name: update_test
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /update_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
{%- endif %}
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
+72 -8
View File
@@ -1,6 +1,7 @@
import io
import re
from pathlib import Path
from typing import Union
from typing import List, Union
import frontmatter
from pydantic import BaseModel
@@ -9,7 +10,7 @@ from openhands.core.exceptions import (
MicroagentValidationError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.microagent.types import MicroagentMetadata, MicroagentType
from openhands.microagent.types import InputMetadata, MicroagentMetadata, MicroagentType
class BaseMicroagent(BaseModel):
@@ -71,13 +72,24 @@ class BaseMicroagent(BaseModel):
subclass_map = {
MicroagentType.KNOWLEDGE: KnowledgeMicroagent,
MicroagentType.REPO_KNOWLEDGE: RepoMicroagent,
MicroagentType.TASK: TaskMicroagent,
}
# Infer the agent type:
# 1. If triggers exist -> KNOWLEDGE
# 2. Else (no triggers) -> REPO
# 1. If inputs exist -> TASK
# 2. If triggers exist -> KNOWLEDGE
# 3. Else (no triggers) -> REPO
inferred_type: MicroagentType
if metadata.triggers:
if metadata.inputs:
inferred_type = MicroagentType.TASK
# Add a trigger for the agent name if not already present
trigger = f'/{metadata.name}'
if not metadata.triggers or trigger not in metadata.triggers:
if not metadata.triggers:
metadata.triggers = [trigger]
else:
metadata.triggers.append(trigger)
elif metadata.triggers:
inferred_type = MicroagentType.KNOWLEDGE
else:
# No triggers, default to REPO unless metadata explicitly says otherwise (which it shouldn't for REPO)
@@ -111,8 +123,8 @@ class KnowledgeMicroagent(BaseMicroagent):
def __init__(self, **data):
super().__init__(**data)
if self.type != MicroagentType.KNOWLEDGE:
raise ValueError('KnowledgeMicroagent must have type KNOWLEDGE')
if self.type not in [MicroagentType.KNOWLEDGE, MicroagentType.TASK]:
raise ValueError('KnowledgeMicroagent must have type KNOWLEDGE or TASK')
def match_trigger(self, message: str) -> str | None:
"""Match a trigger in the message.
@@ -150,6 +162,57 @@ class RepoMicroagent(BaseMicroagent):
)
class TaskMicroagent(KnowledgeMicroagent):
"""TaskMicroagent is a special type of KnowledgeMicroagent that requires user input.
These microagents are triggered by a special format: "/{agent_name}"
and will prompt the user for any required inputs before proceeding.
"""
def __init__(self, **data):
super().__init__(**data)
if self.type != MicroagentType.TASK:
raise ValueError(
f'TaskMicroagent initialized with incorrect type: {self.type}'
)
# Append a prompt to ask for missing variables
self._append_missing_variables_prompt()
def _append_missing_variables_prompt(self) -> None:
"""Append a prompt to ask for missing variables."""
# Check if the content contains any variables or has inputs defined
if not self.requires_user_input() and not self.metadata.inputs:
return
prompt = "\n\nIf the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task."
self.content += prompt
def extract_variables(self, content: str) -> List[str]:
"""Extract variables from the content.
Variables are in the format ${variable_name}.
"""
pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
matches = re.findall(pattern, content)
return matches
def requires_user_input(self) -> bool:
"""Check if this microagent requires user input.
Returns True if the content contains variables in the format ${variable_name}.
"""
# Check if the content contains any variables
variables = self.extract_variables(self.content)
logger.debug(f'This microagent requires user input: {variables}')
return len(variables) > 0
@property
def inputs(self) -> List[InputMetadata]:
"""Get the inputs for this microagent."""
return self.metadata.inputs
def load_microagents_from_dir(
microagent_dir: Union[str, Path],
) -> tuple[dict[str, RepoMicroagent], dict[str, KnowledgeMicroagent]]:
@@ -161,7 +224,7 @@ def load_microagents_from_dir(
microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
Returns:
Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
Tuple of (repo_agents, knowledge_agents) dictionaries
"""
if isinstance(microagent_dir, str):
microagent_dir = Path(microagent_dir)
@@ -182,6 +245,7 @@ def load_microagents_from_dir(
if isinstance(agent, RepoMicroagent):
repo_agents[agent.name] = agent
elif isinstance(agent, KnowledgeMicroagent):
# Both KnowledgeMicroagent and TaskMicroagent go into knowledge_agents
knowledge_agents[agent.name] = agent
logger.debug(f'Loaded agent {agent.name} from {file}')
except Exception as e:
+10
View File
@@ -1,4 +1,5 @@
from enum import Enum
from typing import List
from pydantic import BaseModel, Field
@@ -8,6 +9,14 @@ class MicroagentType(str, Enum):
KNOWLEDGE = 'knowledge'
REPO_KNOWLEDGE = 'repo'
TASK = 'task' # Special type for task microagents that require user input
class InputMetadata(BaseModel):
"""Metadata for task microagent inputs."""
name: str
description: str
class MicroagentMetadata(BaseModel):
@@ -18,3 +27,4 @@ class MicroagentMetadata(BaseModel):
version: str = Field(default='1.0.0')
agent: str = Field(default='CodeActAgent')
triggers: list[str] = [] # optional, only exists for knowledge microagents
inputs: List[InputMetadata] = [] # optional, only exists for task microagents
+62 -4
View File
@@ -1,10 +1,11 @@
from typing import Any
from typing import Any, Dict, List
from fastapi import APIRouter
from openhands.security.options import SecurityAnalyzers
from fastapi import APIRouter, Request
from pydantic import BaseModel
from openhands.controller.agent import Agent
from openhands.microagent.microagent import KnowledgeMicroagent, TaskMicroagent
from openhands.security.options import SecurityAnalyzers
from openhands.server.shared import config, server_config
from openhands.utils.llm import get_supported_llm_models
@@ -67,3 +68,60 @@ async def get_config() -> dict[str, Any]:
dict[str, Any]: The current server configuration.
"""
return server_config.get_config()
class MicroagentInfo(BaseModel):
name: str
trigger: str
description: str
@app.get('/microagents', response_model=List[Dict[str, str]])
async def get_microagents(
request: Request,
) -> List[Dict[str, str]]:
"""Get all available microagents for the current session.
To get the microagents:
```sh
curl http://localhost:3000/api/options/microagents
```
Returns:
List[Dict[str, str]]: A list of microagent information including name and trigger.
"""
# Check if we have a conversation in the request state
if not hasattr(request.state, 'conversation') or not request.state.conversation:
return []
# Get the runtime from the conversation
if (
not hasattr(request.state.conversation, 'runtime')
or not request.state.conversation.runtime
):
return []
# Get the agent session from the runtime
runtime = request.state.conversation.runtime
if not hasattr(runtime, 'agent_session') or not runtime.agent_session:
return []
# Get the memory from the agent session
agent_session = runtime.agent_session
if not hasattr(agent_session, 'memory') or not agent_session.memory:
return []
# Get all knowledge microagents from memory
microagents = []
for agent in agent_session.memory.knowledge_microagents.values():
if isinstance(agent, (KnowledgeMicroagent, TaskMicroagent)) and agent.triggers:
# Use the first trigger as the main one
trigger = agent.triggers[0]
# Extract a short description from the content (first line or paragraph)
description = agent.content.strip().split('\n')[0][:100]
microagents.append(
{'name': agent.name, 'trigger': trigger, 'description': description}
)
return microagents
+178 -1
View File
@@ -1,5 +1,6 @@
"""Tests for microagent loading in runtime."""
import tempfile
from pathlib import Path
from conftest import (
@@ -7,7 +8,13 @@ from conftest import (
_load_runtime,
)
from openhands.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.microagent import (
BaseMicroagent,
KnowledgeMicroagent,
RepoMicroagent,
TaskMicroagent,
)
from openhands.microagent.types import MicroagentType
def _create_test_microagents(test_dir: str):
@@ -165,3 +172,173 @@ Repository-specific test instructions.
finally:
_close_test_runtime(runtime)
def test_task_microagent_creation():
"""Test that a TaskMicroagent is created correctly."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent with a variable: ${test_var}.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.type == MicroagentType.TASK
assert agent.name == 'test_task'
assert '/test_task' in agent.triggers
assert "If the user didn't provide any of these variables" in agent.content
def test_task_microagent_variable_extraction():
"""Test that variables are correctly extracted from the content."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: var1
description: "Variable 1"
---
This is a test with variables: ${var1}, ${var2}, and ${var3}.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
variables = agent.extract_variables(agent.content)
assert set(variables) == {'var1', 'var2', 'var3'}
assert agent.requires_user_input()
def test_knowledge_microagent_no_prompt():
"""Test that a regular KnowledgeMicroagent doesn't get the prompt."""
content = """---
name: test_knowledge
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- test_knowledge
---
This is a test knowledge microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, KnowledgeMicroagent)
assert agent.type == MicroagentType.KNOWLEDGE
assert "If the user didn't provide any of these variables" not in agent.content
def test_task_microagent_trigger_addition():
"""Test that a trigger is added if not present."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert '/test_task' in agent.triggers
def test_task_microagent_no_duplicate_trigger():
"""Test that a trigger is not duplicated if already present."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
- another_trigger
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.triggers.count('/test_task') == 1 # No duplicates
assert len(agent.triggers) == 2
assert 'another_trigger' in agent.triggers
assert '/test_task' in agent.triggers
def test_task_microagent_match_trigger():
"""Test that a task microagent matches its trigger correctly."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.match_trigger('/test_task') == '/test_task'
assert agent.match_trigger(' /test_task ') == '/test_task'
assert agent.match_trigger('This contains /test_task') == '/test_task'
assert agent.match_trigger('/other_task') is None