fix: Manager routing, and UI issues (#440)

* fix: Manager routing, and UI issues

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-07-17 11:52:25 -07:00 • committed by GitHub
parent 928b68fd36
commit 92014bdd11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 68 additions and 54 deletions

View file

@ -23,3 +23,7 @@ export const LANGGRAPH_USER_PERMISSIONS = [
"deployments:search",
"store:access",
];
export enum RequestSource {
GITHUB_ISSUE_WEBHOOK = "github_issue_webhook",
}

View file

@ -37,6 +37,7 @@ import { PLANNER_GRAPH_ID } from "@open-swe/shared/constants";
import { createLogger, LogLevel } from "../../../../utils/logger.js";
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { createClassificationPromptAndToolSchema } from "./utils.js";
import { RequestSource } from "../../../../constants.js";
const logger = createLogger(LogLevel.INFO, "ClassifyMessage");
@ -86,6 +87,9 @@ export async function classifyMessage(
messages: state.messages,
taskPlan,
proposedPlan: issuePlans?.proposedPlan ?? undefined,
requestSource: userMessage.additional_kwargs?.requestSource as
| RequestSource
| undefined,
});
const respondAndRouteTool = {
name: "respond_and_route",

View file

@ -1,3 +1,5 @@
import { RequestSource } from "../../../../constants.js";
export const UPDATE_PROGRAMMER_ROUTING_OPTION = `- update_programmer: You should call this route if the user's message should be added to the programmer's currently running session. This should be called if you determine the user is trying to provide extra context to the programmer's current session.\n`;
export const START_PLANNER_ROUTING_OPTION = `- start_planner: You should call this route if the user's message is a complete request you can send to the planner, which it can use to generate a plan. This route may be called when the planner has not started yet.\n`;
@ -41,6 +43,9 @@ Ensure your response is clear, and concise.
Although you're only supposed to classify & respond to the latest message, this does not mean you should look at it in isolation. You should consider the conversation history as a whole, and the current status of your two AI assistants (programmer and planner) to determine how to respond & route the user's new message.
If the source is from a '${RequestSource.GITHUB_ISSUE_WEBHOOK}', you should ALWAYS classify it as a full request which should be routed to the planner.
The instances where the source will be '${RequestSource.GITHUB_ISSUE_WEBHOOK}' are when the user labels a GitHub issue as a task to be completed by the AI coding agent system.
# Context
Although it's not shown here, you do have access to the full repository contents the user is referencing. Because of this, you should always assume you'll have access to any/all files or folders the user is referencing.
@ -48,6 +53,9 @@ Although it's not shown here, you do have access to the full repository contents
The planner's current status is: {PLANNER_STATUS}
The programmer's current status is: {PROGRAMMER_STATUS}
# Source
The source of the request is: {REQUEST_SOURCE}
{TASK_PLAN_PROMPT}
{CONVERSATION_HISTORY_PROMPT}

View file

@ -31,6 +31,7 @@ import {
START_PLANNER_FOR_FOLLOWUP_ROUTING_OPTION,
} from "./prompts.js";
import { createClassificationSchema } from "./schemas.js";
import { RequestSource } from "../../../../constants.js";
const THREAD_STATUS_READABLE_STRING_MAP = {
not_started: "not started",
@ -72,6 +73,7 @@ export function createClassificationPromptAndToolSchema(inputs: {
messages: BaseMessage[];
taskPlan: TaskPlan;
proposedPlan?: string[];
requestSource?: RequestSource;
}): {
prompt: string;
schema: z.ZodTypeAny;
@ -166,6 +168,10 @@ export function createClassificationPromptAndToolSchema(inputs: {
.replaceAll(
"{CONVERSATION_HISTORY_PROMPT}",
formattedConversationHistoryPrompt ?? "",
)
.replaceAll(
"{REQUEST_SOURCE}",
inputs.requestSource ?? "no source provided",
);
const schema = createClassificationSchema(

View file

@ -19,6 +19,7 @@ import {
getOpenSWELabel,
} from "../../utils/github/label.js";
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
import { RequestSource } from "../../constants.js";
const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook");
@ -149,6 +150,7 @@ webhooks.on("issues.labeled", async ({ payload }) => {
additional_kwargs: {
isOriginalIssue: true,
githubIssueId: issueData.issueNumber,
requestSource: RequestSource.GITHUB_ISSUE_WEBHOOK,
},
}),
],

View file

@ -148,4 +148,25 @@
.shadow-inner-left {
box-shadow: inset 9px 0 6px -1px rgb(0 0 0 / 0.02);
}
.scrollbar-pretty-auto {
overflow-y: auto;
}
.scrollbar-pretty-auto::-webkit-scrollbar {
width: 8px;
}
.scrollbar-pretty-auto::-webkit-scrollbar-thumb {
border-radius: 9999px;
background-color: rgb(209 213 219);
}
.scrollbar-pretty-auto::-webkit-scrollbar-track {
background-color: transparent;
}
.dark .scrollbar-pretty-auto::-webkit-scrollbar-thumb {
background-color: rgb(75 85 99);
}
}

View file

@ -145,47 +145,16 @@ function MatchCaseIcon({ matchCase }: { matchCase: boolean }) {
);
}
function MatchWholeWordIcon({ matchWholeWord }: { matchWholeWord: boolean }) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
className={cn(
"rounded-sm border border-gray-300 px-1 py-[2px] text-xs dark:border-gray-600",
matchWholeWord
? "border-blue-500 bg-blue-500/80 text-white"
: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200",
)}
>
<span className="relative inline-block px-0.5">
<p className="relative z-10 m-0 text-center font-mono">ab</p>
<div
className={cn(
"absolute bottom-0 left-0 h-1/5 w-[1px]",
matchWholeWord ? "bg-white" : "bg-gray-600 dark:bg-gray-300",
)}
></div>
<div
className={cn(
"absolute right-0 bottom-0 left-0 h-[1px] w-full",
matchWholeWord ? "bg-white" : "bg-gray-600 dark:bg-gray-300",
)}
></div>
<div
className={cn(
"absolute right-0 bottom-0 h-1/5 w-[1px]",
matchWholeWord ? "bg-white" : "bg-gray-600 dark:bg-gray-300",
)}
></div>
</span>
</TooltipTrigger>
<TooltipContent>
Match whole word {matchWholeWord ? "on" : "off"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
const coerceStringToArray = (str: string | string[]) => {
if (Array.isArray(str)) {
return str;
}
try {
return JSON.parse(str);
} catch {
return [str];
}
};
function ActionItem(props: ActionItemProps) {
const [expanded, setExpanded] = useState(false);
@ -379,7 +348,10 @@ function ActionItem(props: ActionItemProps) {
<span>Max results: {castProps.max_results}</span>
)}
{castProps.file_types && castProps.file_types.length > 0 && (
<span>File types: {castProps.file_types.join(", ")}</span>
<span>
File types:{" "}
{coerceStringToArray(castProps.file_types).join(", ")}
</span>
)}
{castProps.follow_symlinks && <span>Follow symlinks</span>}
</div>

View file

@ -196,7 +196,7 @@ const defaultComponents: any = {
pre: ({ className, ...props }: { className?: string }) => (
<pre
className={cn(
"max-w-4xl overflow-x-auto rounded-lg bg-black text-white",
"w-full overflow-x-auto rounded-lg bg-black text-white",
className,
)}
{...props}
@ -217,7 +217,7 @@ const defaultComponents: any = {
const code = String(children).replace(/\n$/, "");
return (
<>
<div className="w-full overflow-hidden rounded-lg">
<CodeHeader
language={language}
code={code}
@ -228,7 +228,7 @@ const defaultComponents: any = {
>
{code}
</SyntaxHighlighter>
</>
</div>
);
}

View file

@ -29,8 +29,10 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
customStyle={{
margin: 0,
width: "100%",
maxWidth: "100%",
background: "transparent",
padding: "1.5rem 1rem",
overflow: "auto",
}}
className={className}
>

View file

@ -8,11 +8,10 @@ import { AnimatePresence, motion } from "framer-motion";
import { Bot, Copy, CopyCheck, Send, User, Loader2 } from "lucide-react";
import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
import { useStream } from "@langchain/langgraph-sdk/react";
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
import { cn } from "@/lib/utils";
import { isAIMessageSDK } from "@/lib/langchain-messages";
import { BasicMarkdownText } from "../thread/markdown-text";
function MessageCopyButton({ content }: { content: string }) {
const [copied, setCopied] = useState(false);
@ -98,7 +97,7 @@ export function ManagerChat({
initial={true}
>
<StickyToBottomContent
className="h-full overflow-y-auto"
className="scrollbar-pretty-auto h-full"
contentClassName="space-y-4 p-4"
content={
<>
@ -121,7 +120,7 @@ export function ManagerChat({
</div>
)}
</div>
<div className="relative flex-1 space-y-1">
<div className="relative min-w-0 flex-1 space-y-1 overflow-x-hidden">
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground text-xs font-medium">
{message.type === "human" ? "You" : "Agent"}
@ -130,7 +129,7 @@ export function ManagerChat({
<MessageCopyButton content={messageContentString} />
</div>
</div>
<BasicMarkdownText className="text-foreground text-sm">
<BasicMarkdownText className="text-foreground overflow-x-hidden text-sm">
{messageContentString}
</BasicMarkdownText>
</div>

View file

@ -16,10 +16,6 @@ module.exports = {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
components: {
".scrollbar-pretty":
"overflow-y-scroll [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent",
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",