mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
* init repo-selector * add branch selector UI, extend GH hook w branch info * add reconnectOnMount: true resumeStream, remove repo description from selector, fix styling * styling repo + branch * udpate getBranch calls to include targetRepo arg * format * add search to repo + branch selectors * add settings icon link to /github for dev convenience * add default branch selection * format * minor cleanup * init task list UI demo * individual task component * tasks real data * task visual improvement * persisting tasks, navigate to thread onClick * task UI * TaskSidebar, Navigate to tasks * org tasks by thread * group by thread * improve sidebar, task-list, rm github modal on refresh * collapse sidebar works when viewing a thread * remove micro-tasks expansion from threads component, change taskId to taskIndex for persistence + simplicity * init config sidebar ui * open and close config sidebar * add ref for getAllTasks + add graph_id metadata * format * create threadItem component * format * add threadUtils * centralize + refactor types * format * improve thread status, realtime updates * fix scroll issue * remove temperature from config-sidebar * Change model via sidebar * fix flashing on updates * init plan UI * centralize types * self CR: refactoring, cleanup, remove comments, add todos * CR: clean comments + logs, fix repo selector bug * fix: Update example env file in web (#77) * disable auto scroll on pageload * reposelector: use default branch not main master * remove archived tab temp, default branch listed first in branchSelector dropdown * auto select first repo * improve status indicator * refactor status indicator * simplify status implementation to use Langgraph * util moved from provider to thread-utils * linting, improve task completion logic * feat: Support followup requests (#60) * feat: Support followup requests * cr * cr * fix * account for pr already exists * remove task provider * fix: Better prompting and context provided for codebase structure awareness (#85) * fix: Better prompting and context provided for codebase structure awareness * cr * linting + skeleton loader * fix: branch selector, add github api pagination * small branch update * update task completion status realtime * threadItem stream remove flashing/polling * feat: Create a shared package (#89) * feat: Create a shared package * formatting * fix: drop passing target repo to getBranchName * drop docs * use shared package for config fields in config sidebar * fix: styling and structure for repo/branch selectors * fix: use shared types/objects for config fields * default key for configs * fix: improved styling for threads * fix renering num tasks * disable repo branch selector when chat started * cr * cr * bump deps * fix: rendering plan in interrupt * cr * cr --------- Co-authored-by: Dylan Boudro <121908331+starmorph@users.noreply.github.com>
This commit is contained in:
parent
d985ae4555
commit
8afe1e6803
75 changed files with 3857 additions and 479 deletions
10
README.md
10
README.md
|
|
@ -138,3 +138,13 @@ Once you've accepted the plan, it will begin the execution flow. When the agent
|
|||
|
||||
Open SWE will automatically create a branch whenever you create a new thread with a naming format of `open-swe/<threadId>`. Every time a file is created, modified, or deleted, the changes will be committed to this branch. You can access the changes in the repository by checking out this branch.
|
||||
|
||||
|
||||
|
||||
## Install Daytona CLI
|
||||
```bash
|
||||
# Mac os
|
||||
brew install daytonaio/cli/daytona
|
||||
# Windows
|
||||
powershell -Command "irm https://get.daytona.io/windows | iex"
|
||||
|
||||
```
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "langgraphjs dev --no-browser --config ../../langgraph.json",
|
||||
"clean": "rm -rf ../../.langgraph_api || true && rm -rf .turbo || true && rm -rf ./dist || true",
|
||||
"build": "tsc",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
|
|
@ -24,8 +25,8 @@
|
|||
"@langchain/anthropic": "^0.3.20",
|
||||
"@langchain/core": "^0.3.56",
|
||||
"@langchain/google-genai": "^0.2.9",
|
||||
"@langchain/langgraph": "^0.2.73",
|
||||
"@langchain/langgraph-sdk": "^0.0.78",
|
||||
"@langchain/langgraph": "^0.3.1",
|
||||
"@langchain/langgraph-sdk": "^0.0.83",
|
||||
"@langchain/openai": "^0.5.10",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@open-swe/shared": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ import {
|
|||
PlanItem,
|
||||
} from "@open-swe/shared/open-swe/types";
|
||||
import { formatPlanPromptWithSummaries } from "../utils/plan-prompt.js";
|
||||
import {
|
||||
getMessageContentString,
|
||||
getMessageString,
|
||||
} from "../utils/message/content.js";
|
||||
import { getMessageString } from "../utils/message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { loadModel, Task } from "../utils/load-model.js";
|
||||
import { z } from "zod";
|
||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import {
|
|||
PlanItem,
|
||||
} from "@open-swe/shared/open-swe/types";
|
||||
import { loadModel, Task } from "../utils/load-model.js";
|
||||
import {
|
||||
getMessageContentString,
|
||||
getMessageString,
|
||||
} from "../utils/message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getMessageString } from "../utils/message/content.js";
|
||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
import { getUserRequest } from "../utils/user-request.js";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { formatPlanPrompt } from "../utils/plan-prompt.js";
|
|||
import { stopSandbox } from "../utils/sandbox.js";
|
||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
import { getCurrentPlanItem } from "../utils/current-task.js";
|
||||
import { getMessageContentString } from "../utils/message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getActivePlanItems } from "../utils/task-plan.js";
|
||||
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import {
|
|||
import { startSandbox } from "../utils/sandbox.js";
|
||||
import { createNewTask } from "../utils/task-plan.js";
|
||||
import { getUserRequest } from "../utils/user-request.js";
|
||||
import {
|
||||
PLAN_INTERRUPT_ACTION_TITLE,
|
||||
PLAN_INTERRUPT_DELIMITER,
|
||||
} from "@open-swe/shared/constants";
|
||||
|
||||
export async function interruptPlan(state: GraphState): Promise<Command> {
|
||||
const { proposedPlan } = state;
|
||||
|
|
@ -17,9 +21,9 @@ export async function interruptPlan(state: GraphState): Promise<Command> {
|
|||
|
||||
const interruptRes = interrupt<HumanInterrupt, HumanResponse[]>({
|
||||
action_request: {
|
||||
action: "Approve/Edit Plan",
|
||||
action: PLAN_INTERRUPT_ACTION_TITLE,
|
||||
args: {
|
||||
plan: proposedPlan.join("\n:::\n"),
|
||||
plan: proposedPlan.join(`\n${PLAN_INTERRUPT_DELIMITER}\n`),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
|
@ -29,7 +33,7 @@ export async function interruptPlan(state: GraphState): Promise<Command> {
|
|||
allow_ignore: true,
|
||||
},
|
||||
description: `A new plan has been generated for your request. Please review it and either approve it, edit it, respond to it, or ignore it. Responses will be passed to an LLM where it will rewrite then plan.
|
||||
If editing the plan, ensure each step in the plan is separated by ":::".`,
|
||||
If editing the plan, ensure each step in the plan is separated by "${PLAN_INTERRUPT_DELIMITER}".`,
|
||||
})[0];
|
||||
|
||||
if (!state.sandboxSessionId) {
|
||||
|
|
@ -66,7 +70,7 @@ export async function interruptPlan(state: GraphState): Promise<Command> {
|
|||
|
||||
// Plan was edited, route to the generate-action node to start taking actions.
|
||||
const editedPlan = (interruptRes.args as ActionRequest).args.plan
|
||||
.split(":::")
|
||||
.split(PLAN_INTERRUPT_DELIMITER)
|
||||
.map((step: string) => step.trim());
|
||||
|
||||
const planItems = editedPlan.map((p: string, index: number) => ({
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ import { loadModel, Task } from "../utils/load-model.js";
|
|||
import { AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { formatPlanPrompt } from "../utils/plan-prompt.js";
|
||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
import {
|
||||
getMessageContentString,
|
||||
getMessageString,
|
||||
} from "../utils/message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getMessageString } from "../utils/message/content.js";
|
||||
import { removeLastTaskMessages } from "../utils/message/modify-array.js";
|
||||
import { Command } from "@langchain/langgraph";
|
||||
import { ConfigurableModel } from "langchain/chat_models/universal";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { shellTool } from "../../../tools/index.js";
|
|||
import { PlannerGraphState, PlannerGraphUpdate } from "../types.js";
|
||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||
import { getMessageContentString } from "../../../utils/message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { isHumanMessage } from "@langchain/core/messages";
|
||||
import { formatFollowupMessagePrompt } from "../utils/followup-prompt.js";
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export async function configureGitUserInRepo(
|
|||
}
|
||||
|
||||
// Set user email - use fetched email or fallback to a generic noreply address
|
||||
const emailToUse = userEmail || "open-swe-bot@noreply.github.com";
|
||||
const emailToUse = userEmail || `${repo}-bot@noreply.github.com`;
|
||||
const configUserEmailOutput = await sandbox.process.executeCommand(
|
||||
`git config user.email "${emailToUse}"`,
|
||||
absoluteRepoDir,
|
||||
|
|
|
|||
|
|
@ -6,20 +6,11 @@ import {
|
|||
isHumanMessage,
|
||||
isSystemMessage,
|
||||
isToolMessage,
|
||||
MessageContent,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import { ToolCall } from "@langchain/core/messages/tool";
|
||||
|
||||
export function getMessageContentString(content: MessageContent): string {
|
||||
if (typeof content === "string") return content;
|
||||
|
||||
return content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join(" ");
|
||||
}
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
|
||||
export function getToolCallsString(toolCalls: ToolCall[] | undefined): string {
|
||||
if (!toolCalls?.length) return "";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
isHumanMessage,
|
||||
HumanMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import { getMessageContentString } from "./message/content.js";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
|
||||
export function getUserRequest(
|
||||
messages: BaseMessage[],
|
||||
|
|
|
|||
|
|
@ -20,16 +20,22 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.57",
|
||||
"@langchain/langgraph": "^0.2.73",
|
||||
"@langchain/langgraph-sdk": "^0.0.78",
|
||||
"@langchain/langgraph": "^0.3.1",
|
||||
"@langchain/langgraph-sdk": "^0.0.83",
|
||||
"@open-swe/shared": "workspace:*",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -58,7 +64,8 @@
|
|||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-stick-to-bottom": "^1.0.46",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.24.2"
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
|
|
|
|||
BIN
apps/web/public/favicon.png
Normal file
BIN
apps/web/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -13,7 +13,6 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const installationId = searchParams.get("installation_id");
|
||||
const setupAction = searchParams.get("setup_action");
|
||||
|
||||
// Get the return URL from cookies
|
||||
const returnTo =
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const installationId = searchParams.get("installation_id");
|
||||
const setupAction = searchParams.get("setup_action");
|
||||
|
||||
if (!installationId) {
|
||||
return NextResponse.redirect(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default function GitHubPage() {
|
|||
setError(errorData.error);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setError("Failed to check installation status");
|
||||
}
|
||||
};
|
||||
|
|
@ -50,7 +50,7 @@ export default function GitHubPage() {
|
|||
const errorData = await response.json();
|
||||
setError(errorData.error);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setError("Failed to fetch repositories");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ const inter = Inter({
|
|||
export const metadata: Metadata = {
|
||||
title: "Open SWE",
|
||||
description: "Open SWE UX by LangChain",
|
||||
icons: {
|
||||
icon: "/favicon.png",
|
||||
shortcut: "/favicon.png",
|
||||
apple: "/favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
|
|||
291
apps/web/src/components/configuration-sidebar/config-field.tsx
Normal file
291
apps/web/src/components/configuration-sidebar/config-field.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useConfigStore } from "@/hooks/use-config-store";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import _ from "lodash";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ConfigFieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
type:
|
||||
| "text"
|
||||
| "textarea"
|
||||
| "number"
|
||||
| "switch"
|
||||
| "slider"
|
||||
| "select"
|
||||
| "json";
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
options?: Option[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
// Optional props for external state management
|
||||
value?: any;
|
||||
setValue?: (value: any) => void;
|
||||
}
|
||||
|
||||
export function ConfigField({
|
||||
id,
|
||||
label,
|
||||
type,
|
||||
description,
|
||||
placeholder,
|
||||
options = [],
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
className,
|
||||
value: externalValue, // TODO: Rename to avoid conflict
|
||||
setValue: externalSetValue, // TODO: Rename to avoid conflict
|
||||
}: ConfigFieldProps) {
|
||||
const store = useConfigStore();
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
|
||||
// Determine whether to use external state or Zustand store
|
||||
const isExternallyManaged = externalSetValue !== undefined;
|
||||
|
||||
const currentValue = isExternallyManaged
|
||||
? externalValue
|
||||
: store.configs[id]?.[id];
|
||||
|
||||
const handleChange = (newValue: any) => {
|
||||
setJsonError(null);
|
||||
if (isExternallyManaged && externalSetValue) {
|
||||
externalSetValue(newValue);
|
||||
} else {
|
||||
store.updateConfig(id, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJsonChange = (jsonString: string) => {
|
||||
try {
|
||||
if (!jsonString.trim()) {
|
||||
handleChange(undefined);
|
||||
setJsonError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedJson = JSON.parse(jsonString);
|
||||
|
||||
handleChange(parsedJson);
|
||||
setJsonError(null);
|
||||
} catch {
|
||||
if (isExternallyManaged && externalSetValue) {
|
||||
externalSetValue(jsonString);
|
||||
} else {
|
||||
store.updateConfig(id, jsonString);
|
||||
}
|
||||
setJsonError("Invalid JSON format");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormatJson = (jsonString: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
handleChange(parsed);
|
||||
setJsonError(null);
|
||||
} catch {
|
||||
setJsonError("Invalid JSON format");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
{_.startCase(label)}
|
||||
</Label>
|
||||
{type === "switch" && (
|
||||
<Switch
|
||||
id={id}
|
||||
checked={!!currentValue}
|
||||
onCheckedChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-xs whitespace-pre-line text-gray-500">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{type === "text" && (
|
||||
<Input
|
||||
id={id}
|
||||
value={currentValue || ""}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "textarea" && (
|
||||
<Textarea
|
||||
id={id}
|
||||
value={currentValue || ""}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "number" && (
|
||||
<Input
|
||||
id={id}
|
||||
type="number"
|
||||
value={currentValue !== undefined ? currentValue : ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val === "") {
|
||||
handleChange(undefined);
|
||||
} else {
|
||||
const num = Number(val);
|
||||
if (!isNaN(num)) {
|
||||
handleChange(num);
|
||||
}
|
||||
}
|
||||
}}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "slider" && (
|
||||
<div className="pt-2">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">{min ?? ""}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{currentValue !== undefined
|
||||
? currentValue
|
||||
: min !== undefined && max !== undefined
|
||||
? (min + max) / 2
|
||||
: ""}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{max ?? ""}</span>
|
||||
</div>
|
||||
<Slider
|
||||
id={id}
|
||||
value={[
|
||||
currentValue !== undefined
|
||||
? currentValue
|
||||
: min !== undefined && max !== undefined
|
||||
? (min + max) / 2
|
||||
: 0,
|
||||
]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onValueChange={(vals: number[]) => handleChange(vals[0])}
|
||||
disabled={min === undefined || max === undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "select" && (
|
||||
<Select
|
||||
value={currentValue ?? ""}
|
||||
onValueChange={handleChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={placeholder || "Select an option"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{placeholder && (
|
||||
<SelectItem
|
||||
value=""
|
||||
disabled
|
||||
>
|
||||
{placeholder}
|
||||
</SelectItem>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{type === "json" && (
|
||||
<>
|
||||
<Textarea
|
||||
id={id}
|
||||
value={
|
||||
typeof currentValue === "string"
|
||||
? currentValue
|
||||
: (JSON.stringify(currentValue, null, 2) ?? "")
|
||||
}
|
||||
onChange={(e) => handleJsonChange(e.target.value)}
|
||||
placeholder={placeholder || '{\n "key": "value"\n}'}
|
||||
className={cn(
|
||||
"min-h-[120px] font-mono text-sm",
|
||||
jsonError &&
|
||||
"border-red-500 focus:border-red-500 focus-visible:ring-red-500",
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full items-start justify-between gap-2 pt-1">
|
||||
{" "}
|
||||
{/* Use items-start */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleFormatJson(currentValue ?? "")}
|
||||
disabled={
|
||||
!currentValue || typeof currentValue !== "string" || !!jsonError
|
||||
}
|
||||
className="mt-1"
|
||||
>
|
||||
Format
|
||||
</Button>
|
||||
{jsonError && (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="flex-grow px-3 py-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{" "}
|
||||
<AlertCircle className="h-4 w-4 flex-shrink-0" />{" "}
|
||||
<AlertDescription className="text-xs">
|
||||
{jsonError}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ConfigSectionProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function ConfigSection({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
action,
|
||||
}: ConfigSectionProps) {
|
||||
return (
|
||||
<div className={cn("mb-6", className)}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
|
||||
{action}
|
||||
</div>
|
||||
<div className="space-y-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
apps/web/src/components/configuration-sidebar/index.tsx
Normal file
153
apps/web/src/components/configuration-sidebar/index.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"use client";
|
||||
|
||||
import { forwardRef, ForwardedRef, useState, useEffect } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ConfigField } from "@/components/configuration-sidebar/config-field";
|
||||
import { ConfigSection } from "@/components/configuration-sidebar/config-section";
|
||||
import { useConfigStore } from "@/hooks/use-config-store";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type {
|
||||
ConfigurableFieldUIMetadata,
|
||||
ConfigurableFieldUIType,
|
||||
} from "@open-swe/shared/configurable-metadata";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PanelRightOpen } from "lucide-react";
|
||||
import { GraphConfigurationMetadata } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
/**
|
||||
* Extract configuration metadata from the GraphConfiguration Zod schema
|
||||
*/
|
||||
function extractConfigurationsFromSchema(): ConfigurableFieldUIMetadata[] {
|
||||
const configurations: ConfigurableFieldUIMetadata[] = [];
|
||||
|
||||
for (const [label, { x_open_swe_ui_config: metadata }] of Object.entries(
|
||||
GraphConfigurationMetadata,
|
||||
)) {
|
||||
if (metadata.type === "hidden") {
|
||||
continue;
|
||||
}
|
||||
configurations.push({
|
||||
label,
|
||||
type: metadata.type,
|
||||
default: metadata.default,
|
||||
description: metadata.description,
|
||||
placeholder: metadata.placeholder,
|
||||
options: metadata.options,
|
||||
min: metadata.min,
|
||||
max: metadata.max,
|
||||
step: metadata.step,
|
||||
});
|
||||
}
|
||||
|
||||
return configurations;
|
||||
}
|
||||
|
||||
export interface AIConfigPanelProps {
|
||||
className?: string;
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ConfigurationSidebar = forwardRef<
|
||||
HTMLDivElement,
|
||||
AIConfigPanelProps
|
||||
>(({ className, open, onClose }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { configs, updateConfig } = useConfigStore();
|
||||
|
||||
const [configurations, setConfigurations] = useState<
|
||||
ConfigurableFieldUIMetadata[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
const actualConfigs = extractConfigurationsFromSchema();
|
||||
actualConfigs.forEach((config) => {
|
||||
if (configs[config.label] === undefined && config.default !== undefined) {
|
||||
updateConfig(config.label, config.default);
|
||||
}
|
||||
});
|
||||
|
||||
setConfigurations(actualConfigs);
|
||||
setLoading(false);
|
||||
}, [configs, updateConfig]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 right-0 z-10 h-screen border-l border-gray-200 bg-white shadow-lg transition-all duration-300",
|
||||
open ? "w-80 md:w-xl" : "w-0 overflow-hidden border-l-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{open && (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-gray-200 p-4">
|
||||
<h2 className="text-lg font-semibold">Agent Configuration</h2>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue="general"
|
||||
className="flex flex-1 flex-col overflow-y-auto"
|
||||
>
|
||||
<TabsList className="flex-shrink-0 justify-start bg-transparent px-4 pt-2">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="flex-1 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="general"
|
||||
className="m-0 p-4"
|
||||
>
|
||||
<ConfigSection title="Configuration">
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
configurations.map(
|
||||
(c: ConfigurableFieldUIMetadata, index: number) => (
|
||||
<ConfigField
|
||||
key={`${c.label}-${index}`}
|
||||
id={c.label}
|
||||
label={c.label}
|
||||
type={
|
||||
c.type === "boolean" ? "switch" : (c.type ?? "text")
|
||||
}
|
||||
description={c.description}
|
||||
placeholder={c.placeholder}
|
||||
options={c.options}
|
||||
min={c.min}
|
||||
max={c.max}
|
||||
step={c.step}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</ConfigSection>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ConfigurationSidebar.displayName = "ConfigurationSidebar";
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyIcon, CheckIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { InstallAppButton } from "./InstallAppButton";
|
||||
import { InstallAppButton } from "./install-app-button";
|
||||
|
||||
interface AgentTokenProviderProps {
|
||||
className?: string;
|
||||
|
|
@ -41,7 +41,7 @@ export function AgentTokenProvider({
|
|||
setToken(data.token);
|
||||
setInstallationId(data.installation_id);
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setError("Network error when fetching token");
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -51,6 +51,8 @@ export function GitHubOAuthButton() {
|
|||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className="text-xs"
|
||||
size="sm"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
|
|
@ -67,11 +69,10 @@ export function GitHubOAuthButton() {
|
|||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
className="text-xs"
|
||||
size="sm"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<GitHubSVG />
|
||||
{isLoading ? "Disconnecting..." : "Disconnect GitHub"}
|
||||
</Button>
|
||||
);
|
||||
|
|
@ -82,11 +83,10 @@ export function GitHubOAuthButton() {
|
|||
variant="outline"
|
||||
onClick={handleLogin}
|
||||
disabled={isLoading}
|
||||
className="text-xs"
|
||||
size="sm"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<GitHubSVG />
|
||||
{isLoading ? "Connecting..." : "Connect GitHub"}
|
||||
</Button>
|
||||
);
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { GitBranch, Shield } from "lucide-react";
|
||||
|
||||
interface BranchSelectorProps {
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
buttonClassName?: string;
|
||||
chatStarted?: boolean;
|
||||
}
|
||||
|
||||
export function BranchSelector({
|
||||
disabled = false,
|
||||
placeholder = "Select a branch...",
|
||||
buttonClassName,
|
||||
chatStarted = false,
|
||||
}: BranchSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
branches,
|
||||
branchesLoading,
|
||||
branchesError,
|
||||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
selectedRepository,
|
||||
defaultBranch,
|
||||
} = useGitHubApp();
|
||||
|
||||
// Auto-select default branch when repository changes and branches are loaded
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedRepository &&
|
||||
!branchesLoading &&
|
||||
!branchesError &&
|
||||
branches.length > 0
|
||||
) {
|
||||
// Check if the current selected branch exists in the new repository
|
||||
const currentBranchExists =
|
||||
selectedBranch &&
|
||||
branches.some((branch) => branch.name === selectedBranch);
|
||||
|
||||
// Auto-select default branch if no branch is selected OR if the selected branch doesn't exist in this repo
|
||||
if (!selectedBranch || !currentBranchExists) {
|
||||
// Try to find the repository's actual default branch first
|
||||
const actualDefaultBranch = defaultBranch
|
||||
? branches.find((branch) => branch.name === defaultBranch)
|
||||
: null;
|
||||
|
||||
if (actualDefaultBranch) {
|
||||
setSelectedBranch(actualDefaultBranch.name);
|
||||
} else if (branches.length > 0) {
|
||||
// If default branch doesn't exist in branches list, select the first available branch
|
||||
setSelectedBranch(branches[0].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
selectedRepository?.owner,
|
||||
selectedRepository?.repo,
|
||||
branchesLoading,
|
||||
branchesError,
|
||||
branches.length,
|
||||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
defaultBranch,
|
||||
]);
|
||||
|
||||
const handleSelect = (branchName: string) => {
|
||||
setSelectedBranch(branchName);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (!selectedRepository) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<GitBranch />
|
||||
<span>Select a branch</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (branchesLoading) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<GitBranch />
|
||||
<span>Loading branches...</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (branchesError) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<GitBranch />
|
||||
<span>Error loading branches</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (branches.length === 0) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<GitBranch />
|
||||
<span>
|
||||
No branches available {branchesError && `(${branchesError})`}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (chatStarted) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<GitBranch />
|
||||
<span className="truncate text-left">
|
||||
{selectedBranch || placeholder}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<GitBranch />
|
||||
<span className="truncate text-left">
|
||||
{selectedBranch || placeholder}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[340px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search branches..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{branches
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
if (defaultBranch) {
|
||||
if (a.name === defaultBranch) return -1;
|
||||
if (b.name === defaultBranch) return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((branch) => {
|
||||
const isSelected = selectedBranch === branch.name;
|
||||
const isDefault = branch.name === defaultBranch;
|
||||
return (
|
||||
<CommandItem
|
||||
key={branch.name}
|
||||
value={branch.name}
|
||||
onSelect={() => handleSelect(branch.name)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
<span className="font-medium">{branch.name}</span>
|
||||
{isDefault && (
|
||||
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700">
|
||||
default
|
||||
</span>
|
||||
)}
|
||||
{branch.protected && (
|
||||
<div title="Protected branch">
|
||||
<Shield className="h-3 w-3 text-amber-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { BranchSelector } from "./branch-selector";
|
||||
import { RepositorySelector } from "./repository-selector";
|
||||
import { useQueryState } from "nuqs";
|
||||
|
||||
export function RepositoryBranchSelectors() {
|
||||
const [threadId] = useQueryState("threadId");
|
||||
const chatStarted = !!threadId;
|
||||
const defaultButtonStyles =
|
||||
"bg-inherit border-gray-300 rounded-full text-gray-500 hover:text-gray-700 text-xs";
|
||||
const defaultStylesChatStarted =
|
||||
"hover:bg-inherit cursor-default hover:cursor-default hover:text-gray-500 hover:border-gray-300 hover:ring-inherit";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<RepositorySelector
|
||||
chatStarted={chatStarted}
|
||||
buttonClassName={
|
||||
defaultButtonStyles +
|
||||
(chatStarted ? " " + defaultStylesChatStarted : "")
|
||||
}
|
||||
/>
|
||||
<BranchSelector
|
||||
chatStarted={chatStarted}
|
||||
buttonClassName={
|
||||
defaultButtonStyles +
|
||||
(chatStarted ? " " + defaultStylesChatStarted : "")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import type { Repository } from "@/utils/github";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
|
||||
interface RepositorySelectorProps {
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
buttonClassName?: string;
|
||||
chatStarted?: boolean;
|
||||
}
|
||||
// TODO: remove this, we should use the TargetRepository type from the open-swe package
|
||||
// Convert GitHub Repository to TargetRepository format
|
||||
const repositoryToTarget = (repo: Repository): TargetRepository => {
|
||||
const [owner, repoName] = repo.full_name.split("/");
|
||||
return { owner, repo: repoName };
|
||||
};
|
||||
|
||||
// TODO: Add utility function for repoSelector while viewing a thread
|
||||
// retrieve thread repo from nuqs params. set disabled = true.
|
||||
|
||||
export function RepositorySelector({
|
||||
disabled = false,
|
||||
placeholder = "Select a repository...",
|
||||
buttonClassName,
|
||||
chatStarted = false,
|
||||
}: RepositorySelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
repositories,
|
||||
selectedRepository,
|
||||
setSelectedRepository,
|
||||
isLoading,
|
||||
error,
|
||||
isInstalled,
|
||||
} = useGitHubApp();
|
||||
|
||||
const handleSelect = (repositoryKey: string) => {
|
||||
const repository = repositories.find(
|
||||
(repo) => repo.full_name === repositoryKey,
|
||||
);
|
||||
if (repository) {
|
||||
setSelectedRepository(repositoryToTarget(repository));
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedValue = selectedRepository
|
||||
? `${selectedRepository.owner}/${selectedRepository.repo}`
|
||||
: undefined;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<span>Loading repositories...</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<span>Error loading repositories</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (isInstalled === false) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<span>GitHub App not installed</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (repositories.length === 0) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<span>No repositories available</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (chatStarted) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(buttonClassName)}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<GitHubSVG />
|
||||
<span className="truncate text-left">
|
||||
{selectedValue || placeholder}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(buttonClassName)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<GitHubSVG />
|
||||
<span className="truncate text-left">
|
||||
{selectedValue || placeholder}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[340px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search repositories..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No repositories found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{repositories.map((repo) => {
|
||||
const key = repo.full_name;
|
||||
const isSelected = selectedValue === key;
|
||||
return (
|
||||
<CommandItem
|
||||
key={repo.id}
|
||||
value={key}
|
||||
onSelect={() => handleSelect(key)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{repo.full_name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import { InstallAppButton } from "./InstallAppButton";
|
||||
import { InstallAppButton } from "./install-app-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
export const GitHubSVG = ({ width = "100%", height = "100%" }) => (
|
||||
interface GitHubSVGProps {
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GitHubSVG = ({
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
className,
|
||||
}: GitHubSVGProps) => (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -6,6 +16,7 @@ export const GitHubSVG = ({ width = "100%", height = "100%" }) => (
|
|||
height={height}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
|
|
|
|||
1
apps/web/src/components/plan/index.ts
Normal file
1
apps/web/src/components/plan/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { PlanViewer } from "./plan-viewer";
|
||||
128
apps/web/src/components/plan/plan-viewer.tsx
Normal file
128
apps/web/src/components/plan/plan-viewer.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { Check, Clock, Play } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
interface PlanViewerProps {
|
||||
planItems: PlanItem[];
|
||||
className?: string;
|
||||
isProposedPlan?: boolean;
|
||||
}
|
||||
|
||||
export function PlanViewer({
|
||||
planItems,
|
||||
className,
|
||||
isProposedPlan,
|
||||
}: PlanViewerProps) {
|
||||
const currentTaskIndex = planItems
|
||||
.filter((item) => !item.completed)
|
||||
.reduce(
|
||||
(minIndex, item) => (item.index < minIndex ? item.index : minIndex),
|
||||
Infinity,
|
||||
);
|
||||
|
||||
const getTaskStatus = (item: PlanItem) => {
|
||||
if (isProposedPlan) return "proposed";
|
||||
if (item.completed) return "completed";
|
||||
if (item.index === currentTaskIndex) return "current";
|
||||
return "remaining";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("w-full space-y-3", className)}>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
{isProposedPlan ? "Proposed" : "Execution"} Plan
|
||||
</h3>
|
||||
{!isProposedPlan && (
|
||||
<span className="text-xs text-gray-500">
|
||||
{planItems.filter((item) => item.completed).length} of{" "}
|
||||
{planItems.length} completed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{planItems
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map((item) => {
|
||||
const status = getTaskStatus(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`plan-item-${item.index}`}
|
||||
className={cn(
|
||||
"flex items-start gap-3 rounded-lg border p-3 transition-colors",
|
||||
{
|
||||
"border-green-200 bg-green-50": status === "completed",
|
||||
"border-blue-200 bg-blue-50": status === "current",
|
||||
"border-gray-200 bg-gray-50": [
|
||||
"remaining",
|
||||
"proposed",
|
||||
].includes(status),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{/* Status Icon */}
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
{status === "completed" && (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
)}
|
||||
{status === "current" && (
|
||||
<Play className="h-4 w-4 text-blue-600" />
|
||||
)}
|
||||
{["remaining", "proposed"].includes(status) && (
|
||||
<Clock className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
Step {item.index + 1}
|
||||
</span>
|
||||
{status === "current" && (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
|
||||
In Progress
|
||||
</span>
|
||||
)}
|
||||
{status === "completed" && (
|
||||
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
||||
Done
|
||||
</span>
|
||||
)}
|
||||
{status === "proposed" && (
|
||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">
|
||||
Proposed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={cn("text-sm leading-relaxed", {
|
||||
"text-gray-900": status === "current",
|
||||
"text-gray-600":
|
||||
status === "completed" ||
|
||||
["remaining", "proposed"].includes(status),
|
||||
})}
|
||||
>
|
||||
{item.plan}
|
||||
</p>
|
||||
|
||||
{/* Summary for completed tasks */}
|
||||
{item.summary && status === "completed" && (
|
||||
<div className="bg-green-25 mt-2 rounded border border-green-100 p-2">
|
||||
<p className="mb-1 text-xs font-medium text-green-700">
|
||||
Summary:
|
||||
</p>
|
||||
<p className="text-xs text-green-600">{item.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
apps/web/src/components/sidebar-buttons/index.tsx
Normal file
114
apps/web/src/components/sidebar-buttons/index.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { forwardRef, ForwardedRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Settings,
|
||||
History,
|
||||
EllipsisVertical,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
|
||||
interface SidebarButtonsProps {
|
||||
historyOpen: boolean;
|
||||
setHistoryOpen: (open: boolean) => void;
|
||||
configOpen: boolean;
|
||||
setConfigOpen: (open: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
// TODO: Implement Using this component in the thread component instead of current implementation
|
||||
export const SidebarButtons = forwardRef<HTMLDivElement, SidebarButtonsProps>(
|
||||
(
|
||||
{ historyOpen, setHistoryOpen, configOpen, setConfigOpen, className },
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const handleConfigClick = () => {
|
||||
setConfigOpen(true);
|
||||
setHistoryOpen(false);
|
||||
};
|
||||
|
||||
const handleHistoryClick = () => {
|
||||
setHistoryOpen(true);
|
||||
setConfigOpen(false);
|
||||
};
|
||||
|
||||
const closeAll = () => {
|
||||
setConfigOpen(false);
|
||||
setHistoryOpen(false);
|
||||
};
|
||||
|
||||
const isSidebarOpen = historyOpen || configOpen;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-4 z-50 transition-all duration-300 ease-in-out",
|
||||
isSidebarOpen
|
||||
? "right-[theme(spacing.80)] md:right-[37rem]"
|
||||
: "right-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative flex items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"bg-background hover:bg-muted rounded-full transition-all",
|
||||
isSidebarOpen ? "opacity-100 shadow-lg" : "opacity-0 shadow-none",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isSidebarOpen) return;
|
||||
// Sidebar is open, clicking this will close any open sidebar
|
||||
closeAll();
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? (
|
||||
<ChevronRight className="size-5" />
|
||||
) : (
|
||||
<EllipsisVertical className="size-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0 right-0 flex items-center gap-2 transition-all",
|
||||
isSidebarOpen && "right-full mr-2",
|
||||
)}
|
||||
>
|
||||
<TooltipIconButton
|
||||
tooltip="Agent Configuration"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"bg-background hover:bg-muted size-9 rounded-full shadow-xs hover:cursor-pointer",
|
||||
isSidebarOpen && "shadow-lg",
|
||||
)}
|
||||
onClick={handleConfigClick}
|
||||
aria-label="Agent Configuration"
|
||||
>
|
||||
<Settings className="size-5" />
|
||||
</TooltipIconButton>
|
||||
<TooltipIconButton
|
||||
tooltip="History"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"bg-background hover:bg-muted size-9 rounded-full shadow-xs hover:cursor-pointer",
|
||||
isSidebarOpen && "shadow-lg",
|
||||
)}
|
||||
onClick={handleHistoryClick}
|
||||
aria-label="History"
|
||||
>
|
||||
<History className="size-5" />
|
||||
</TooltipIconButton>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SidebarButtons.displayName = "SidebarButtons";
|
||||
30
apps/web/src/components/status-indicator.tsx
Normal file
30
apps/web/src/components/status-indicator.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import { XCircle, LoaderCircle, Pause, Circle } from "lucide-react";
|
||||
import { ThreadStatus } from "@langchain/langgraph-sdk";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const StatusIndicator = ({
|
||||
status,
|
||||
size = "default",
|
||||
}: {
|
||||
status: ThreadStatus;
|
||||
size?: "default" | "sm";
|
||||
}) => {
|
||||
const iconClass = size === "sm" ? "h-3 w-3" : "h-4 w-4";
|
||||
|
||||
switch (status) {
|
||||
case "busy":
|
||||
return (
|
||||
<LoaderCircle className={cn("animate-spin text-blue-500", iconClass)} />
|
||||
);
|
||||
case "interrupted":
|
||||
return <Pause className={cn("text-yellow-500", iconClass)} />;
|
||||
case "idle":
|
||||
return <Circle className={cn("text-gray-400", iconClass)} />;
|
||||
case "error":
|
||||
return <XCircle className={cn("text-red-500", iconClass)} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
127
apps/web/src/components/task-list-sidebar.tsx
Normal file
127
apps/web/src/components/task-list-sidebar.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Archive,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
PanelRightOpen,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
||||
import { useQueryState, parseAsString } from "nuqs";
|
||||
import { useState } from "react";
|
||||
import { ThreadItem } from "./thread-item";
|
||||
|
||||
const THREADS_PER_PAGE = 10; // More threads per page in sidebar
|
||||
|
||||
// TODO: Clarify Language about Threads and Tasks in the TaskListSidebar component
|
||||
interface TaskListSidebarProps {
|
||||
onCollapse?: () => void;
|
||||
}
|
||||
|
||||
export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
|
||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
||||
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const { threads, threadsLoading } = useThreads();
|
||||
|
||||
// Handle thread navigation
|
||||
const handleThreadClick = (thread: ThreadWithTasks) => {
|
||||
setThreadId(thread.thread_id);
|
||||
setTaskId(null);
|
||||
};
|
||||
|
||||
// Sort threads by creation date (newest first) - already done in provider
|
||||
const sortedThreads = threads;
|
||||
const totalThreads = sortedThreads.length;
|
||||
const totalPages = Math.ceil(totalThreads / THREADS_PER_PAGE);
|
||||
const startIndex = currentPage * THREADS_PER_PAGE;
|
||||
const endIndex = startIndex + THREADS_PER_PAGE;
|
||||
const paginatedThreads = sortedThreads.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col border-r bg-white">
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Threads</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{totalThreads} total threads
|
||||
</p>
|
||||
</div>
|
||||
{onCollapse && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onCollapse}
|
||||
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{threadsLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<Archive className="mx-auto mb-2 h-5 w-5 animate-pulse opacity-50" />
|
||||
<p className="text-sm">Loading threads...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : paginatedThreads.length > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="space-y-1 p-2">
|
||||
{paginatedThreads.map((thread) => (
|
||||
<ThreadItem
|
||||
key={thread.thread_id}
|
||||
thread={thread}
|
||||
onClick={handleThreadClick}
|
||||
variant="sidebar"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<Archive className="mx-auto mb-2 h-5 w-5 opacity-50" />
|
||||
<p className="text-sm">No threads found</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="border-t border-gray-200 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
|
||||
disabled={currentPage === 0}
|
||||
className="h-7"
|
||||
>
|
||||
<ChevronLeft className="h-3 w-3" />
|
||||
</Button>
|
||||
<span className="text-xs text-gray-500">
|
||||
Page {currentPage + 1} of {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCurrentPage(Math.min(totalPages - 1, currentPage + 1))
|
||||
}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
className="h-7"
|
||||
>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
102
apps/web/src/components/task-list.tsx
Normal file
102
apps/web/src/components/task-list.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"use client";
|
||||
|
||||
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
|
||||
import { useQueryState, parseAsString } from "nuqs";
|
||||
import { useState } from "react";
|
||||
import { ThreadItem } from "./thread-item";
|
||||
|
||||
const THREADS_PER_PAGE = 5;
|
||||
|
||||
export default function TaskList() {
|
||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
||||
const [_threadId, setThreadId] = useQueryState("threadId", parseAsString);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const { threads, threadsLoading } = useThreads();
|
||||
|
||||
const isDashboardMode = !taskId;
|
||||
|
||||
const handleThreadClick = (thread: ThreadWithTasks) => {
|
||||
setThreadId(thread.thread_id);
|
||||
setTaskId(null);
|
||||
};
|
||||
|
||||
if (!isDashboardMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Threads are already sorted by creation date in ThreadProvider
|
||||
const sortedThreads = threads;
|
||||
const totalThreads = sortedThreads.length;
|
||||
const totalPages = Math.ceil(totalThreads / THREADS_PER_PAGE);
|
||||
const startIndex = currentPage * THREADS_PER_PAGE;
|
||||
const endIndex = startIndex + THREADS_PER_PAGE;
|
||||
const paginatedThreads = sortedThreads.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full max-w-3xl flex-col gap-2">
|
||||
<p className="text-sm text-gray-500">Threads ({totalThreads})</p>
|
||||
<div>
|
||||
{threadsLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<Archive className="mx-auto mb-2 h-6 w-6 animate-pulse opacity-50" />
|
||||
<p className="text-sm">Loading threads...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : paginatedThreads.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{paginatedThreads.map((thread) => (
|
||||
<ThreadItem
|
||||
key={thread.thread_id}
|
||||
thread={thread}
|
||||
onClick={handleThreadClick}
|
||||
variant="dashboard"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between border-t pt-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
Showing {startIndex + 1}-{Math.min(endIndex, totalThreads)} of{" "}
|
||||
{totalThreads} threads
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.max(0, currentPage - 1))}
|
||||
disabled={currentPage === 0}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCurrentPage(Math.min(totalPages - 1, currentPage + 1))
|
||||
}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<Archive className="mx-auto mb-2 h-6 w-6 opacity-50" />
|
||||
<p className="text-sm">No threads found</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
apps/web/src/components/thread-item.tsx
Normal file
134
apps/web/src/components/thread-item.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"use client";
|
||||
|
||||
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ThreadWithTasks } from "@/providers/Thread";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { StatusIndicator } from "@/components/status-indicator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { GitHubSVG } from "./icons/github";
|
||||
|
||||
interface ThreadItemProps {
|
||||
thread: ThreadWithTasks;
|
||||
onClick: (thread: ThreadWithTasks) => void;
|
||||
variant?: "sidebar" | "dashboard";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThreadItem({
|
||||
thread,
|
||||
onClick,
|
||||
variant = "dashboard",
|
||||
className,
|
||||
}: ThreadItemProps) {
|
||||
const isSidebar = variant === "sidebar";
|
||||
|
||||
const displayDate = new Date(thread.created_at).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
// Check if thread data is still loading/incomplete
|
||||
const isLoading =
|
||||
!thread.threadTitle ||
|
||||
thread.threadTitle.includes("undefined") ||
|
||||
!thread.repository ||
|
||||
thread.repository === "Unknown Repository" ||
|
||||
thread.repository.includes("undefined");
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border border-gray-200 bg-inherit p-1.5 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Skeleton className="h-3 w-3 rounded-full" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-1 text-xs">
|
||||
<Skeleton className="h-3 w-3" /> {/* GitHub icon placeholder */}
|
||||
<Skeleton className="h-3 w-16" /> {/* Repo */}
|
||||
<Skeleton className="ml-0.5 h-2.5 w-2.5" />{" "}
|
||||
<Skeleton className="ml-0.5 h-3 w-12" /> {/* Branch */}
|
||||
<Skeleton className="ml-auto h-3 w-10" /> {/* Date */}
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<Skeleton className="h-4 w-20 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group cursor-pointer rounded-md border border-gray-200 bg-inherit p-2 shadow-sm transition-colors hover:bg-gray-50 hover:shadow-md",
|
||||
className,
|
||||
)}
|
||||
onClick={() => onClick(thread)}
|
||||
>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<StatusIndicator status={thread.status} />
|
||||
<h4 className="w-full truncate text-xs leading-tight font-medium text-gray-900">
|
||||
{thread.threadTitle}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-1 text-xs text-gray-500">
|
||||
<div className="mr-1 flex flex-row items-center justify-start gap-1">
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<span className="max-w-[90px] truncate">{thread.repository}</span>
|
||||
<span>/</span>
|
||||
<GitBranch className="size-2.5 flex-shrink-0" />
|
||||
<span className="max-w-[70px] truncate">{thread.branch}</span>
|
||||
</div>
|
||||
|
||||
<span>•</span>
|
||||
|
||||
<span className="mx-1 whitespace-nowrap">{displayDate}</span>
|
||||
|
||||
{!isSidebar && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<div className="ml-1 flex items-center gap-1">
|
||||
<ListTodo className="size-4 flex-shrink-0" />
|
||||
<span>
|
||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isSidebar && (
|
||||
<div className="flex items-center gap-1 text-xs text-gray-500">
|
||||
<ListTodo className="size-4 flex-shrink-0" />
|
||||
<span>
|
||||
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowRight
|
||||
className={cn(
|
||||
"mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { File, Image as ImageIcon, X as XIcon } from "lucide-react";
|
||||
import { File, X as XIcon } from "lucide-react";
|
||||
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
|
|
|
|||
|
|
@ -8,6 +8,34 @@ import { MarkdownText } from "../../markdown-text";
|
|||
import { ActionRequest, HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
||||
import { toast } from "sonner";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { PlanViewer } from "@/components/plan";
|
||||
import { isPlanData, parsePlanData, getPlanKey } from "@/lib/plan-utils";
|
||||
|
||||
interface InboxItemInputProps {
|
||||
interruptValue: HumanInterrupt;
|
||||
humanResponse: HumanResponseWithEdits[];
|
||||
supportsMultipleMethods: boolean;
|
||||
acceptAllowed: boolean;
|
||||
hasEdited: boolean;
|
||||
hasAddedResponse: boolean;
|
||||
initialValues: Record<string, string>;
|
||||
|
||||
streaming: boolean;
|
||||
streamFinished: boolean;
|
||||
|
||||
setHumanResponse: React.Dispatch<
|
||||
React.SetStateAction<HumanResponseWithEdits[]>
|
||||
>;
|
||||
setSelectedSubmitType: React.Dispatch<
|
||||
React.SetStateAction<SubmitType | undefined>
|
||||
>;
|
||||
setHasAddedResponse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setHasEdited: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
handleSubmit: (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
function ResetButton({ handleReset }: { handleReset: () => void }) {
|
||||
return (
|
||||
|
|
@ -22,7 +50,36 @@ function ResetButton({ handleReset }: { handleReset: () => void }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ArgsRenderer({ args }: { args: Record<string, any> }) {
|
||||
function ArgsRenderer({
|
||||
args,
|
||||
actionName,
|
||||
}: {
|
||||
args: Record<string, any>;
|
||||
actionName: string;
|
||||
}) {
|
||||
if (isPlanData(args, actionName)) {
|
||||
const planItems = parsePlanData(args);
|
||||
const planKey = getPlanKey(args);
|
||||
|
||||
if (planItems.length > 0) {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start gap-6">
|
||||
<div className="flex w-full flex-col items-start gap-1">
|
||||
<p className="text-sm leading-[18px] text-wrap text-gray-600">
|
||||
{prettifyText(planKey || "plan")}:
|
||||
</p>
|
||||
<div className="w-full max-w-full rounded-xl border border-gray-200 bg-white p-4">
|
||||
<PlanViewer
|
||||
planItems={planItems}
|
||||
isProposedPlan
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start gap-6">
|
||||
{Object.entries(args).map(([k, v]) => {
|
||||
|
|
@ -51,32 +108,6 @@ function ArgsRenderer({ args }: { args: Record<string, any> }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface InboxItemInputProps {
|
||||
interruptValue: HumanInterrupt;
|
||||
humanResponse: HumanResponseWithEdits[];
|
||||
supportsMultipleMethods: boolean;
|
||||
acceptAllowed: boolean;
|
||||
hasEdited: boolean;
|
||||
hasAddedResponse: boolean;
|
||||
initialValues: Record<string, string>;
|
||||
|
||||
streaming: boolean;
|
||||
streamFinished: boolean;
|
||||
|
||||
setHumanResponse: React.Dispatch<
|
||||
React.SetStateAction<HumanResponseWithEdits[]>
|
||||
>;
|
||||
setSelectedSubmitType: React.Dispatch<
|
||||
React.SetStateAction<SubmitType | undefined>
|
||||
>;
|
||||
setHasAddedResponse: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setHasEdited: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
handleSubmit: (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
function ResponseComponent({
|
||||
humanResponse,
|
||||
streaming,
|
||||
|
|
@ -120,7 +151,10 @@ function ResponseComponent({
|
|||
</div>
|
||||
|
||||
{showArgsInResponse && (
|
||||
<ArgsRenderer args={interruptValue.action_request.args} />
|
||||
<ArgsRenderer
|
||||
args={interruptValue.action_request.args}
|
||||
actionName={interruptValue.action_request.action}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full flex-col items-start gap-[6px]">
|
||||
|
|
@ -152,10 +186,12 @@ const Response = React.memo(ResponseComponent);
|
|||
function AcceptComponent({
|
||||
streaming,
|
||||
actionRequestArgs,
|
||||
actionName,
|
||||
handleSubmit,
|
||||
}: {
|
||||
streaming: boolean;
|
||||
actionRequestArgs: Record<string, any>;
|
||||
actionName: string;
|
||||
handleSubmit: (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent,
|
||||
) => Promise<void>;
|
||||
|
|
@ -163,7 +199,10 @@ function AcceptComponent({
|
|||
return (
|
||||
<div className="flex w-full flex-col items-start gap-4 rounded-lg border-[1px] border-gray-300 p-6">
|
||||
{actionRequestArgs && Object.keys(actionRequestArgs).length > 0 && (
|
||||
<ArgsRenderer args={actionRequestArgs} />
|
||||
<ArgsRenderer
|
||||
args={actionRequestArgs}
|
||||
actionName={actionName}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="brand"
|
||||
|
|
@ -199,8 +238,13 @@ function EditAndOrAcceptComponent({
|
|||
) => Promise<void>;
|
||||
}) {
|
||||
const defaultRows = React.useRef<Record<string, number>>({});
|
||||
const [editingField, setEditingField] = React.useState<string | null>(null);
|
||||
|
||||
const editResponse = humanResponse.find((r) => r.type === "edit");
|
||||
const acceptResponse = humanResponse.find((r) => r.type === "accept");
|
||||
|
||||
const actionName = interruptValue.action_request.action;
|
||||
|
||||
if (
|
||||
!editResponse ||
|
||||
typeof editResponse.args !== "object" ||
|
||||
|
|
@ -209,6 +253,7 @@ function EditAndOrAcceptComponent({
|
|||
if (acceptResponse) {
|
||||
return (
|
||||
<AcceptComponent
|
||||
actionName={actionName}
|
||||
actionRequestArgs={interruptValue.action_request.args}
|
||||
streaming={streaming}
|
||||
handleSubmit={handleSubmit}
|
||||
|
|
@ -217,6 +262,7 @@ function EditAndOrAcceptComponent({
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = editResponse.acceptAllowed ? "Edit/Accept" : "Edit";
|
||||
let buttonText = "Submit";
|
||||
if (editResponse.acceptAllowed && !editResponse.editsMade) {
|
||||
|
|
@ -268,6 +314,12 @@ function EditAndOrAcceptComponent({
|
|||
const value = ["string", "number"].includes(typeof v)
|
||||
? v
|
||||
: JSON.stringify(v, null);
|
||||
|
||||
// Check if this field contains plan data and user isn't actively editing it
|
||||
const isFieldBeingEdited = editingField === k;
|
||||
const fieldArgs = { [k]: v };
|
||||
const isFieldPlanData = isPlanData(fieldArgs, actionName);
|
||||
|
||||
// Calculate the default number of rows by the total length of the initial value divided by 30
|
||||
// or 8, whichever is greater. Stored in a ref to prevent re-rendering.
|
||||
if (
|
||||
|
|
@ -287,15 +339,49 @@ function EditAndOrAcceptComponent({
|
|||
key={`allow-edit-args--${k}-${idx}`}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-[6px]">
|
||||
<p className="min-w-fit text-sm font-medium">{prettifyText(k)}</p>
|
||||
<Textarea
|
||||
disabled={streaming}
|
||||
className="h-full"
|
||||
value={value}
|
||||
onChange={(e) => onEditChange(e.target.value, editResponse, k)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={numRows}
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="min-w-fit text-sm font-medium">
|
||||
{prettifyText(k)}
|
||||
</p>
|
||||
{isFieldPlanData && !isFieldBeingEdited && (
|
||||
<button
|
||||
onClick={() => setEditingField(k)}
|
||||
className="rounded border border-blue-200 px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 hover:text-blue-800"
|
||||
>
|
||||
Edit as Text
|
||||
</button>
|
||||
)}
|
||||
{isFieldBeingEdited && (
|
||||
<button
|
||||
onClick={() => setEditingField(null)}
|
||||
className="rounded border border-gray-200 px-2 py-1 text-xs text-gray-600 hover:bg-gray-50 hover:text-gray-800"
|
||||
>
|
||||
Show Plan View
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFieldPlanData && !isFieldBeingEdited ? (
|
||||
// Show plan viewer for plan data when not editing
|
||||
<div className="w-full rounded-lg border border-gray-200 bg-white p-3">
|
||||
<PlanViewer
|
||||
planItems={parsePlanData(fieldArgs)}
|
||||
isProposedPlan
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Show textarea for editing or non-plan data
|
||||
<Textarea
|
||||
disabled={streaming}
|
||||
className="h-full"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
onEditChange(e.target.value, editResponse, k)
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={numRows}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -338,6 +424,7 @@ export function InboxItemInput({
|
|||
hasArgs && !isEditAllowed && !acceptAllowed && isResponseAllowed;
|
||||
const showArgsOutsideActionCards =
|
||||
hasArgs && !showArgsInResponse && !isEditAllowed && !acceptAllowed;
|
||||
const actionName = interruptValue.action_request.action;
|
||||
|
||||
const onEditChange = (
|
||||
change: string | string[],
|
||||
|
|
@ -493,7 +580,10 @@ export function InboxItemInput({
|
|||
return (
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||
{showArgsOutsideActionCards && (
|
||||
<ArgsRenderer args={interruptValue.action_request.args} />
|
||||
<ArgsRenderer
|
||||
args={interruptValue.action_request.args}
|
||||
actionName={actionName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { TooltipIconButton } from "../../tooltip-icon-button";
|
||||
import { TooltipIconButton } from "../../../ui/tooltip-icon-button";
|
||||
|
||||
export function ThreadIdTooltip({ threadId }: { threadId: string }) {
|
||||
const firstThreeChars = threadId.slice(0, 3);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function ToolCallTable({ toolCall }: { toolCall: ToolCall }) {
|
|||
|
||||
try {
|
||||
valueStr = valueStr || JSON.stringify(value, null);
|
||||
} catch (_) {
|
||||
} catch {
|
||||
// failed to stringify, just assign an empty string
|
||||
valueStr = "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { toast } from "sonner";
|
|||
import { HumanInterrupt, HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||
import { END } from "@langchain/langgraph/web";
|
||||
import { useStreamContext } from "@/providers/Stream";
|
||||
import { useThreads } from "@/providers/Thread";
|
||||
|
||||
interface UseInterruptedActionsInput {
|
||||
interrupt: HumanInterrupt;
|
||||
|
|
@ -54,6 +55,7 @@ export default function useInterruptedActions({
|
|||
interrupt,
|
||||
}: UseInterruptedActionsInput): UseInterruptedActionsValue {
|
||||
const thread = useStreamContext();
|
||||
const { refreshThreads } = useThreads();
|
||||
const [humanResponse, setHumanResponse] = useState<HumanResponseWithEdits[]>(
|
||||
[],
|
||||
);
|
||||
|
|
@ -93,6 +95,12 @@ export default function useInterruptedActions({
|
|||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Trigger immediate status refresh when execution resumes
|
||||
setTimeout(() => {
|
||||
refreshThreads().catch(console.error);
|
||||
}, 1000); // 1 second delay to allow thread status to update
|
||||
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
console.error("Error sending human response", e);
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ export function unknownToPrettyDate(input: unknown): string | undefined {
|
|||
) {
|
||||
return format(new Date(input as string), "MM/dd/yyyy hh:mm a");
|
||||
}
|
||||
} catch (_) {
|
||||
// failed to parse date. no-op
|
||||
} catch {
|
||||
console.error("Failed to parse date", input);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,17 +82,12 @@ export default function ThreadHistory() {
|
|||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
|
||||
const { getThreads, threads, setThreads, threadsLoading, setThreadsLoading } =
|
||||
useThreads();
|
||||
const { refreshThreads, threads, threadsLoading } = useThreads();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
setThreadsLoading(true);
|
||||
getThreads()
|
||||
.then(setThreads)
|
||||
.catch(console.error)
|
||||
.finally(() => setThreadsLoading(false));
|
||||
}, []);
|
||||
refreshThreads().catch(console.error);
|
||||
}, [refreshThreads]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
ensureToolCallsHaveResponses,
|
||||
} from "@/lib/ensure-tool-responses";
|
||||
import { LangGraphLogoSVG } from "../icons/langgraph";
|
||||
import { TooltipIconButton } from "./tooltip-icon-button";
|
||||
import { TooltipIconButton } from "../ui/tooltip-icon-button";
|
||||
import {
|
||||
ArrowDown,
|
||||
LoaderCircle,
|
||||
|
|
@ -22,14 +22,14 @@ import {
|
|||
SquarePen,
|
||||
XIcon,
|
||||
Plus,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { useQueryState, parseAsBoolean } from "nuqs";
|
||||
import { useQueryState, parseAsBoolean, parseAsString } from "nuqs";
|
||||
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
||||
import ThreadHistory from "./history";
|
||||
import TaskListSidebar from "../task-list-sidebar";
|
||||
import { toast } from "sonner";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { useFileUpload } from "@/hooks/use-file-upload";
|
||||
import { ContentBlocksPreview } from "./ContentBlocksPreview";
|
||||
import {
|
||||
|
|
@ -38,7 +38,20 @@ import {
|
|||
ArtifactTitle,
|
||||
useArtifactContext,
|
||||
} from "./artifact";
|
||||
import { GitHubOAuthButton } from "../github-oauth-button";
|
||||
import { GitHubOAuthButton } from "../github/github-oauth-button";
|
||||
import { useGitHubApp } from "@/hooks/useGitHubApp";
|
||||
import TaskList from "../task-list";
|
||||
import { ConfigurationSidebar } from "../configuration-sidebar";
|
||||
import { useConfigStore } from "@/hooks/use-config-store";
|
||||
import { RepositoryBranchSelectors } from "../github/repo-branch-selectors";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "../ui/tooltip";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
function StickyToBottomContent(props: {
|
||||
content: ReactNode;
|
||||
|
|
@ -82,18 +95,48 @@ function ScrollToBottom(props: { className?: string }) {
|
|||
}
|
||||
|
||||
export function Thread() {
|
||||
const { push } = useRouter();
|
||||
const [artifactContext, setArtifactContext] = useArtifactContext();
|
||||
const [artifactOpen, closeArtifact] = useArtifactOpen();
|
||||
const { selectedRepository } = useGitHubApp();
|
||||
const { getConfigs } = useConfigStore();
|
||||
|
||||
const [threadId, _setThreadId] = useQueryState("threadId");
|
||||
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
|
||||
const [chatHistoryOpen, setChatHistoryOpen] = useQueryState(
|
||||
"chatHistoryOpen",
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [hideToolCalls, setHideToolCalls] = useQueryState(
|
||||
"hideToolCalls",
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
|
||||
const [configSidebarOpen, setConfigSidebarOpen] = useState(false);
|
||||
|
||||
const isTaskView = !!taskId;
|
||||
const isThreadView = !!threadId;
|
||||
|
||||
// Track previous states to detect navigation changes
|
||||
const prevTaskId = useRef(taskId);
|
||||
const prevThreadId = useRef(threadId);
|
||||
|
||||
useEffect(() => {
|
||||
const isNavigatingToTask = !prevTaskId.current && taskId;
|
||||
const isNavigatingToThread = !prevThreadId.current && threadId;
|
||||
if ((isNavigatingToTask || isNavigatingToThread) && !chatHistoryOpen) {
|
||||
setChatHistoryOpen(true);
|
||||
}
|
||||
prevTaskId.current = taskId;
|
||||
prevThreadId.current = threadId;
|
||||
}, [taskId, threadId, chatHistoryOpen, setChatHistoryOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (taskId && typeof window !== "undefined") {
|
||||
// TaskId format is "${threadId}-${taskIndex}", so we can extract the threadId directly
|
||||
const taskThreadId = taskId.split("-").slice(0, -1).join("-");
|
||||
if (taskThreadId && taskThreadId !== threadId) {
|
||||
_setThreadId(taskThreadId);
|
||||
}
|
||||
}
|
||||
}, [taskId, threadId, _setThreadId]);
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const {
|
||||
contentBlocks,
|
||||
|
|
@ -116,9 +159,11 @@ export function Thread() {
|
|||
const setThreadId = (id: string | null) => {
|
||||
_setThreadId(id);
|
||||
|
||||
// close artifact and reset artifact context
|
||||
closeArtifact();
|
||||
setArtifactContext({});
|
||||
if (id === null) {
|
||||
setTaskId(null);
|
||||
closeArtifact();
|
||||
setArtifactContext({});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -129,11 +174,9 @@ export function Thread() {
|
|||
try {
|
||||
const message = (stream.error as any).message;
|
||||
if (!message || lastError.current === message) {
|
||||
// Message has already been logged. do not modify ref, return early.
|
||||
return;
|
||||
}
|
||||
|
||||
// Message is defined, and it has not been logged yet. Save it, and send the error
|
||||
lastError.current = message;
|
||||
toast.error("An error occurred. Please try again.", {
|
||||
description: (
|
||||
|
|
@ -145,11 +188,10 @@ export function Thread() {
|
|||
closeButton: true,
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
console.error("Error in stream", stream.error);
|
||||
}
|
||||
}, [stream.error]);
|
||||
|
||||
// TODO: this should be part of the useStream hook
|
||||
const prevMessageLength = useRef(0);
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -167,6 +209,17 @@ export function Thread() {
|
|||
e.preventDefault();
|
||||
if ((input.trim().length === 0 && contentBlocks.length === 0) || isLoading)
|
||||
return;
|
||||
|
||||
if (!selectedRepository) {
|
||||
toast.error("Please select a repository first", {
|
||||
description:
|
||||
"You need to select a repository before sending a message.",
|
||||
richColors: true,
|
||||
closeButton: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setFirstTokenReceived(false);
|
||||
|
||||
const newHumanMessage: Message = {
|
||||
|
|
@ -183,15 +236,14 @@ export function Thread() {
|
|||
const context =
|
||||
Object.keys(artifactContext).length > 0 ? artifactContext : undefined;
|
||||
|
||||
const targetRepository = {
|
||||
owner: "langchain-ai",
|
||||
repo: "open-swe",
|
||||
};
|
||||
stream.submit(
|
||||
{
|
||||
messages: [...toolMessages, newHumanMessage],
|
||||
messages: [
|
||||
...toolMessages,
|
||||
newHumanMessage,
|
||||
] as unknown as BaseMessage[],
|
||||
context,
|
||||
targetRepository,
|
||||
targetRepository: selectedRepository,
|
||||
},
|
||||
{
|
||||
streamMode: ["values"],
|
||||
|
|
@ -202,10 +254,16 @@ export function Thread() {
|
|||
...(prev.messages ?? []),
|
||||
...toolMessages,
|
||||
newHumanMessage,
|
||||
],
|
||||
] as unknown as BaseMessage[],
|
||||
}),
|
||||
config: {
|
||||
recursion_limit: 400,
|
||||
configurable: {
|
||||
...getConfigs(),
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
graph_id: process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "open-swe",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -225,6 +283,12 @@ export function Thread() {
|
|||
streamMode: ["values"],
|
||||
config: {
|
||||
recursion_limit: 400,
|
||||
configurable: {
|
||||
...getConfigs(),
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
graph_id: process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "open-swe",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -257,7 +321,7 @@ export function Thread() {
|
|||
className="relative h-full"
|
||||
style={{ width: 300 }}
|
||||
>
|
||||
<ThreadHistory />
|
||||
<TaskListSidebar onCollapse={() => setChatHistoryOpen(false)} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
|
@ -305,7 +369,16 @@ export function Thread() {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-2 right-4 flex items-center">
|
||||
<div className="absolute top-2 right-4 flex items-center gap-2 text-gray-700">
|
||||
<TooltipIconButton
|
||||
tooltip="Configuration"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setConfigSidebarOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</TooltipIconButton>
|
||||
<GitHubOAuthButton />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -330,7 +403,7 @@ export function Thread() {
|
|||
</div>
|
||||
<motion.button
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
onClick={() => setThreadId(null)}
|
||||
onClick={() => push("/")}
|
||||
animate={{
|
||||
marginLeft: !chatHistoryOpen ? 48 : 0,
|
||||
}}
|
||||
|
|
@ -350,18 +423,23 @@ export function Thread() {
|
|||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center">
|
||||
<GitHubOAuthButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-700">
|
||||
<GitHubOAuthButton />
|
||||
<TooltipIconButton
|
||||
tooltip="Configuration"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setConfigSidebarOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</TooltipIconButton>
|
||||
<TooltipIconButton
|
||||
size="lg"
|
||||
className="p-4"
|
||||
tooltip="New thread"
|
||||
variant="ghost"
|
||||
onClick={() => setThreadId(null)}
|
||||
>
|
||||
<SquarePen className="size-5" />
|
||||
<SquarePen className="size-4" />
|
||||
</TooltipIconButton>
|
||||
</div>
|
||||
|
||||
|
|
@ -369,11 +447,14 @@ export function Thread() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<StickToBottom className="relative flex-1 overflow-hidden">
|
||||
<StickToBottom
|
||||
className="relative flex-1 overflow-hidden"
|
||||
initial={false}
|
||||
>
|
||||
<StickyToBottomContent
|
||||
className={cn(
|
||||
"absolute inset-0 overflow-y-scroll px-4 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent",
|
||||
!chatStarted && "mt-[25vh] flex flex-col items-stretch",
|
||||
!chatStarted && "mt-[10vh] flex flex-col items-stretch",
|
||||
chatStarted && "grid grid-rows-[1fr_auto]",
|
||||
)}
|
||||
contentClassName="pt-8 pb-16 max-w-3xl mx-auto flex flex-col gap-4 w-full"
|
||||
|
|
@ -415,7 +496,14 @@ export function Thread() {
|
|||
</>
|
||||
}
|
||||
footer={
|
||||
<div className="sticky bottom-0 flex flex-col items-center gap-8 bg-white">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-8 bg-white",
|
||||
!isTaskView && !isThreadView
|
||||
? "mb-32 pb-32"
|
||||
: "sticky bottom-0",
|
||||
)}
|
||||
>
|
||||
{!chatStarted && (
|
||||
<div className="flex items-center gap-3">
|
||||
<LangGraphLogoSVG className="h-8 flex-shrink-0" />
|
||||
|
|
@ -465,31 +553,21 @@ export function Thread() {
|
|||
className="field-sizing-content resize-none border-none bg-transparent p-3.5 pb-0 shadow-none ring-0 outline-none focus:ring-0 focus:outline-none"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-6 p-2 pt-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="render-tool-calls"
|
||||
checked={hideToolCalls ?? false}
|
||||
onCheckedChange={setHideToolCalls}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="render-tool-calls"
|
||||
className="text-sm text-gray-600"
|
||||
>
|
||||
Hide Tool Calls
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<Label
|
||||
htmlFor="file-input"
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<Plus className="size-5 text-gray-600" />
|
||||
<span className="text-sm text-gray-600">
|
||||
Upload PDF or Image
|
||||
</span>
|
||||
</Label>
|
||||
<div className="flex items-center gap-1 p-2 pt-4">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Label
|
||||
htmlFor="file-input"
|
||||
className="mr-1 ml-2 flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<Plus className="size-4 text-gray-600" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach files</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
|
|
@ -498,6 +576,8 @@ export function Thread() {
|
|||
accept="image/jpeg,image/png,image/gif,image/webp,application/pdf"
|
||||
className="hidden"
|
||||
/>
|
||||
<RepositoryBranchSelectors />
|
||||
|
||||
{stream.isLoading ? (
|
||||
<Button
|
||||
key="stop"
|
||||
|
|
@ -522,6 +602,8 @@ export function Thread() {
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{!isTaskView && !isThreadView && <TaskList />}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
@ -542,6 +624,11 @@ export function Thread() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfigurationSidebar
|
||||
open={configSidebarOpen}
|
||||
onClose={() => setConfigSidebarOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { FC, memo, useState } from "react";
|
|||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { SyntaxHighlighter } from "@/components/thread/syntax-highlighter";
|
||||
|
||||
import { TooltipIconButton } from "@/components/thread/tooltip-icon-button";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import "katex/dist/katex.min.css";
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { BranchSwitcher, CommandBar } from "./shared";
|
||||
import { MultimodalPreview } from "@/components/thread/MultimodalPreview";
|
||||
import { isBase64ContentBlock } from "@/lib/multimodal-utils";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
function EditableContent({
|
||||
value,
|
||||
|
|
@ -54,7 +55,7 @@ export function HumanMessage({
|
|||
|
||||
const newMessage: Message = { type: "human", content: value };
|
||||
thread.submit(
|
||||
{ messages: [newMessage] },
|
||||
{ messages: [newMessage] as unknown as BaseMessage[] },
|
||||
{
|
||||
checkpoint: parentCheckpoint,
|
||||
streamMode: ["values"],
|
||||
|
|
@ -64,7 +65,10 @@ export function HumanMessage({
|
|||
|
||||
return {
|
||||
...values,
|
||||
messages: [...(values.messages ?? []), newMessage],
|
||||
messages: [
|
||||
...(values.messages ?? []),
|
||||
newMessage,
|
||||
] as unknown as BaseMessage[],
|
||||
};
|
||||
},
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { TooltipIconButton } from "../tooltip-icon-button";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type TooltipIconButtonProps = ButtonProps & {
|
||||
tooltip: string;
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
};
|
||||
|
||||
export const TooltipIconButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
TooltipIconButtonProps
|
||||
>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
{...rest}
|
||||
className={cn("size-6 p-1", className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{tooltip}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side}>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipIconButton.displayName = "TooltipIconButton";
|
||||
166
apps/web/src/components/ui/alert-dialog.tsx
Normal file
166
apps/web/src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Root
|
||||
data-slot="alert-dialog"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger
|
||||
data-slot="alert-dialog-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal
|
||||
data-slot="alert-dialog-portal"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
66
apps/web/src/components/ui/alert.tsx
Normal file
66
apps/web/src/components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
46
apps/web/src/components/ui/badge.tsx
Normal file
46
apps/web/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
36
apps/web/src/components/ui/collapsible.tsx
Normal file
36
apps/web/src/components/ui/collapsible.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.Root
|
||||
data-slot="collapsible"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
56
apps/web/src/components/ui/scroll-area.tsx
Normal file
56
apps/web/src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
63
apps/web/src/components/ui/slider.tsx
Normal file
63
apps/web/src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider };
|
||||
66
apps/web/src/components/ui/tabs.tsx
Normal file
66
apps/web/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
74
apps/web/src/components/ui/tooltip-icon-button.tsx
Normal file
74
apps/web/src/components/ui/tooltip-icon-button.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
"use client";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type TooltipIconButtonProps = ButtonProps & {
|
||||
tooltip: string;
|
||||
/**
|
||||
* @default "bottom"
|
||||
*/
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
/**
|
||||
* @default 200
|
||||
*/
|
||||
delayDuration?: number;
|
||||
/**
|
||||
* @default "ghost"
|
||||
*/
|
||||
variant?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
};
|
||||
|
||||
export const TooltipIconButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
TooltipIconButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
tooltip,
|
||||
side = "bottom",
|
||||
className,
|
||||
delayDuration = 200,
|
||||
variant = "ghost",
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
{...rest}
|
||||
className={cn("size-6 p-1", className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{tooltip}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side}>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TooltipIconButton.displayName = "TooltipIconButton";
|
||||
106
apps/web/src/hooks/use-config-store.tsx
Normal file
106
apps/web/src/hooks/use-config-store.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import type { ConfigurableFieldUIMetadata } from "@open-swe/shared/configurable-metadata";
|
||||
|
||||
export const DEFAULT_CONFIG_KEY = "open_swe_default_config";
|
||||
|
||||
interface ConfigState {
|
||||
configs: Record<string, any>;
|
||||
getConfig: (key: string) => Record<string, any>;
|
||||
getConfigs: () => Record<string, any>;
|
||||
updateConfig: (key: string, value: any) => void;
|
||||
resetConfig: (key: string) => void;
|
||||
setDefaultConfig: (
|
||||
key: string,
|
||||
configurations: ConfigurableFieldUIMetadata[],
|
||||
) => void;
|
||||
resetStore: (key: string) => void;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
configs: {},
|
||||
|
||||
getConfig: (key: string) => {
|
||||
const state = get();
|
||||
const baseConfig = state.configs[key];
|
||||
const configObj = {
|
||||
...baseConfig,
|
||||
};
|
||||
delete configObj.__defaultValues;
|
||||
return configObj;
|
||||
},
|
||||
|
||||
getConfigs: () => {
|
||||
const state = get();
|
||||
const flatConfigs: Record<string, any> = {};
|
||||
Object.entries(state.configs).forEach(([key, configObj]) => {
|
||||
if (
|
||||
configObj &&
|
||||
typeof configObj === "object" &&
|
||||
configObj[key] !== undefined
|
||||
) {
|
||||
flatConfigs[key] = configObj[key];
|
||||
}
|
||||
});
|
||||
return flatConfigs;
|
||||
},
|
||||
|
||||
updateConfig: (key, value) =>
|
||||
set((state) => ({
|
||||
configs: {
|
||||
...state.configs,
|
||||
[key]: {
|
||||
...(state.configs[key] || {}),
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
resetConfig: (key: string) => {
|
||||
set((state) => {
|
||||
const config = state.configs[key];
|
||||
if (!config || !config.__defaultValues) {
|
||||
return state;
|
||||
}
|
||||
const defaultsToUse = { ...config.__defaultValues };
|
||||
return {
|
||||
configs: {
|
||||
...state.configs,
|
||||
[key]: defaultsToUse,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setDefaultConfig: (
|
||||
key: string,
|
||||
configurations: ConfigurableFieldUIMetadata[],
|
||||
) => {
|
||||
const defaultConfig: Record<string, any> = {};
|
||||
configurations.forEach((config: ConfigurableFieldUIMetadata) => {
|
||||
if (config.default !== undefined) {
|
||||
defaultConfig[config.label] = config.default;
|
||||
}
|
||||
});
|
||||
|
||||
defaultConfig.__defaultValues = { ...defaultConfig };
|
||||
|
||||
set((currentState) => ({
|
||||
configs: {
|
||||
...currentState.configs,
|
||||
[key]: defaultConfig,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
resetStore: () => set({ configs: {} }),
|
||||
}),
|
||||
{
|
||||
name: "open-swe-config-storage",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -2,14 +2,7 @@ import { useState, useRef, useEffect, ChangeEvent } from "react";
|
|||
import { toast } from "sonner";
|
||||
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||
import { fileToContentBlock } from "@/lib/multimodal-utils";
|
||||
|
||||
export const SUPPORTED_FILE_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"application/pdf",
|
||||
];
|
||||
import { SUPPORTED_FILE_TYPES } from "@/types/file-upload";
|
||||
|
||||
interface UseFileUploadOptions {
|
||||
initialBlocks?: Base64ContentBlock[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Repository } from "@/utils/github";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
interface UseGitHubAppReturn {
|
||||
isInstalled: boolean | null;
|
||||
|
|
@ -7,6 +9,26 @@ interface UseGitHubAppReturn {
|
|||
error: string | null;
|
||||
repositories: Repository[];
|
||||
refreshRepositories: () => Promise<void>;
|
||||
selectedRepository: TargetRepository | null;
|
||||
setSelectedRepository: (repo: TargetRepository | null) => void;
|
||||
branches: Branch[];
|
||||
branchesLoading: boolean;
|
||||
branchesError: string | null;
|
||||
selectedBranch: string | null;
|
||||
setSelectedBranch: (branch: string | null) => void;
|
||||
refreshBranches: () => Promise<void>;
|
||||
defaultBranch: string | null;
|
||||
}
|
||||
|
||||
// Helper function to get GitHub OAuth access token from cookies
|
||||
function getGitHubAccessToken(): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
const cookies = document.cookie.split("; ");
|
||||
const tokenCookie = cookies.find((row) =>
|
||||
row.startsWith("x-github_access_token="),
|
||||
);
|
||||
return tokenCookie ? tokenCookie.split("=")[1] : null;
|
||||
}
|
||||
|
||||
export function useGitHubApp(): UseGitHubAppReturn {
|
||||
|
|
@ -14,6 +36,51 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [branchesLoading, setBranchesLoading] = useState(false);
|
||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||
const [selectedRepositoryParam, setSelectedRepositoryParam] =
|
||||
useQueryState("repo");
|
||||
const [selectedBranchParam, setSelectedBranchParam] = useQueryState("branch");
|
||||
|
||||
// Track if auto-selection has been attempted to prevent re-triggering
|
||||
const hasAutoSelectedRef = useRef(false);
|
||||
|
||||
const selectedRepository = selectedRepositoryParam
|
||||
? (() => {
|
||||
try {
|
||||
// Parse "owner/repo" format instead of JSON
|
||||
const parts = selectedRepositoryParam.split("/");
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
owner: parts[0],
|
||||
repo: parts[1],
|
||||
branch: selectedBranchParam || undefined,
|
||||
} as TargetRepository;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
|
||||
const selectedBranch = selectedBranchParam;
|
||||
|
||||
const setSelectedRepository = useCallback(
|
||||
(repo: TargetRepository | null) => {
|
||||
setSelectedRepositoryParam(repo ? `${repo.owner}/${repo.repo}` : null);
|
||||
if (!repo) {
|
||||
setSelectedBranchParam(null);
|
||||
setBranches([]);
|
||||
}
|
||||
},
|
||||
[setSelectedRepositoryParam, setSelectedBranchParam],
|
||||
);
|
||||
|
||||
const setSelectedBranch = (branch: string | null) => {
|
||||
setSelectedBranchParam(branch);
|
||||
};
|
||||
|
||||
const checkInstallation = async () => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -35,7 +102,7 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
setIsInstalled(false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setError("Failed to check GitHub App installation status");
|
||||
setIsInstalled(false);
|
||||
} finally {
|
||||
|
|
@ -43,19 +110,117 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchBranches = useCallback(async () => {
|
||||
if (!selectedRepository) {
|
||||
setBranches([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const accessToken = getGitHubAccessToken();
|
||||
if (!accessToken) {
|
||||
setBranchesError("GitHub access token not found");
|
||||
return;
|
||||
}
|
||||
|
||||
setBranchesLoading(true);
|
||||
setBranchesError(null);
|
||||
|
||||
try {
|
||||
const branchData = await getRepositoryBranches(
|
||||
selectedRepository.owner,
|
||||
selectedRepository.repo,
|
||||
accessToken,
|
||||
);
|
||||
setBranches(branchData || []);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to fetch branches";
|
||||
console.error(
|
||||
`Error fetching branches for ${selectedRepository.owner}/${selectedRepository.repo}:`,
|
||||
err,
|
||||
);
|
||||
setBranchesError(errorMessage);
|
||||
} finally {
|
||||
setBranchesLoading(false);
|
||||
}
|
||||
}, [selectedRepository?.owner, selectedRepository?.repo]);
|
||||
|
||||
useEffect(() => {
|
||||
checkInstallation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRepository) {
|
||||
fetchBranches();
|
||||
} else {
|
||||
setBranches([]);
|
||||
setSelectedBranchParam(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
selectedRepository?.owner,
|
||||
selectedRepository?.repo,
|
||||
setSelectedBranchParam,
|
||||
]);
|
||||
|
||||
// Auto-select first repository on initial page load
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasAutoSelectedRef.current && // Haven't auto-selected yet
|
||||
!selectedRepository && // No repo currently selected
|
||||
!isLoading && // Not loading repositories
|
||||
!error && // No error occurred
|
||||
isInstalled === true && // GitHub App is installed
|
||||
repositories.length > 0 // Repositories are available
|
||||
) {
|
||||
const firstRepo = repositories[0];
|
||||
const targetRepo = {
|
||||
owner: firstRepo.full_name.split("/")[0],
|
||||
repo: firstRepo.full_name.split("/")[1],
|
||||
};
|
||||
setSelectedRepository(targetRepo);
|
||||
hasAutoSelectedRef.current = true;
|
||||
}
|
||||
}, [
|
||||
repositories,
|
||||
selectedRepository,
|
||||
isLoading,
|
||||
error,
|
||||
isInstalled,
|
||||
setSelectedRepository,
|
||||
]);
|
||||
|
||||
const refreshRepositories = async () => {
|
||||
await checkInstallation();
|
||||
};
|
||||
|
||||
const refreshBranches = async () => {
|
||||
await fetchBranches();
|
||||
};
|
||||
|
||||
// Get the default branch for the currently selected repository
|
||||
const defaultBranch = selectedRepository
|
||||
? repositories.find(
|
||||
(repo) =>
|
||||
repo.full_name ===
|
||||
`${selectedRepository.owner}/${selectedRepository.repo}`,
|
||||
)?.default_branch || null
|
||||
: null;
|
||||
|
||||
return {
|
||||
isInstalled,
|
||||
isLoading,
|
||||
error,
|
||||
repositories,
|
||||
refreshRepositories,
|
||||
selectedRepository,
|
||||
setSelectedRepository,
|
||||
branches,
|
||||
branchesLoading,
|
||||
branchesError,
|
||||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
refreshBranches,
|
||||
defaultBranch,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
export function getApiKey(): string | null {
|
||||
try {
|
||||
if (typeof window === "undefined") return null;
|
||||
return window.localStorage.getItem("lg:chat:apiKey") ?? null;
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
56
apps/web/src/lib/plan-utils.ts
Normal file
56
apps/web/src/lib/plan-utils.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||
import {
|
||||
PLAN_INTERRUPT_ACTION_TITLE,
|
||||
PLAN_INTERRUPT_DELIMITER,
|
||||
} from "@open-swe/shared/constants";
|
||||
|
||||
/**
|
||||
* Checks if the interrupt args contain plan data
|
||||
*/
|
||||
export function isPlanData(
|
||||
args: Record<string, any>,
|
||||
actionName: string,
|
||||
): boolean {
|
||||
return !!(
|
||||
actionName === PLAN_INTERRUPT_ACTION_TITLE &&
|
||||
args.plan &&
|
||||
typeof args.plan === "string" &&
|
||||
args.plan?.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses plan data from interrupt args into PlanItem array
|
||||
*/
|
||||
export function parsePlanData(args: Record<string, any>): PlanItem[] {
|
||||
if (!args.plan) return [];
|
||||
if (typeof args.plan !== "string") return [];
|
||||
if (!args.plan.includes(PLAN_INTERRUPT_DELIMITER)) {
|
||||
// if args.plan is defined, but does not include the delimiter, it is a single plan item
|
||||
return [
|
||||
{
|
||||
index: 0,
|
||||
plan: args.plan,
|
||||
completed: false,
|
||||
summary: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
const planItems = args.plan
|
||||
.split(PLAN_INTERRUPT_DELIMITER)
|
||||
.map((item: string, index: number) => ({
|
||||
index,
|
||||
plan: item,
|
||||
completed: false,
|
||||
summary: undefined,
|
||||
}));
|
||||
return planItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the key name that contains plan data
|
||||
*/
|
||||
export function getPlanKey(args: Record<string, any>): string | null {
|
||||
if (args.plan && typeof args.plan === "string") return "plan";
|
||||
return null;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import React, {
|
|||
ReactNode,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { type Message } from "@langchain/langgraph-sdk";
|
||||
|
|
@ -17,30 +18,18 @@ import {
|
|||
import { useQueryState } from "nuqs";
|
||||
import { LangGraphLogoSVG } from "@/components/icons/langgraph";
|
||||
import { useThreads } from "./Thread";
|
||||
import { TooltipIconButton } from "@/components/thread/tooltip-icon-button";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
import { Copy, CopyCheck, ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
import { useGitHubToken } from "@/hooks/useGitHubToken";
|
||||
|
||||
type TargetRepository = { owner: string; repo: string };
|
||||
export type StateType = {
|
||||
messages: Message[];
|
||||
ui?: UIMessage[];
|
||||
targetRepository?: TargetRepository;
|
||||
};
|
||||
import { GraphState, GraphUpdate } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
const useTypedStream = useStream<
|
||||
StateType,
|
||||
GraphState,
|
||||
{
|
||||
UpdateType: {
|
||||
messages?: Message[] | Message | string;
|
||||
ui?: (UIMessage | RemoveUIMessage)[] | UIMessage | RemoveUIMessage;
|
||||
context?: Record<string, unknown>;
|
||||
targetRepository?: TargetRepository;
|
||||
};
|
||||
UpdateType: GraphUpdate;
|
||||
CustomEventType: UIMessage | RemoveUIMessage;
|
||||
}
|
||||
>;
|
||||
|
|
@ -64,7 +53,8 @@ const StreamSession = ({
|
|||
githubToken: string;
|
||||
}) => {
|
||||
const [threadId, setThreadId] = useQueryState("threadId");
|
||||
const { getThreads, setThreads } = useThreads();
|
||||
const { refreshThreads, setThreads, updateThreadFromStream } = useThreads();
|
||||
|
||||
const githubAccessToken =
|
||||
document.cookie
|
||||
.split("; ")
|
||||
|
|
@ -73,6 +63,7 @@ const StreamSession = ({
|
|||
const streamValue = useTypedStream({
|
||||
apiUrl,
|
||||
assistantId,
|
||||
reconnectOnMount: true,
|
||||
threadId: threadId ?? null,
|
||||
defaultHeaders: {
|
||||
"x-github-installation-token": githubToken,
|
||||
|
|
@ -90,10 +81,44 @@ const StreamSession = ({
|
|||
setThreadId(id);
|
||||
// Refetch threads list when thread ID changes.
|
||||
// Wait for some seconds before fetching so we're able to get the new thread that was created.
|
||||
sleep().then(() => getThreads().then(setThreads).catch(console.error));
|
||||
sleep().then(() => {
|
||||
refreshThreads().catch(console.error);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for stream updates to update all thread properties in real-time
|
||||
// Use a ref to track the last update to prevent excessive calls
|
||||
const lastUpdateRef = useRef<{ threadId: string; valuesHash: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (threadId && streamValue.values) {
|
||||
// Create a simple hash of the values to detect actual changes
|
||||
const valuesHash = JSON.stringify({
|
||||
plan: (streamValue.values as any).plan,
|
||||
proposedPlan: (streamValue.values as any).proposedPlan,
|
||||
targetRepository: (streamValue.values as any).targetRepository,
|
||||
messages: (
|
||||
streamValue.values as any
|
||||
).messages?.[0]?.content?.[0]?.text?.substring(0, 50),
|
||||
});
|
||||
|
||||
// Only update if thread or values actually changed
|
||||
if (
|
||||
!lastUpdateRef.current ||
|
||||
lastUpdateRef.current.threadId !== threadId ||
|
||||
lastUpdateRef.current.valuesHash !== valuesHash
|
||||
) {
|
||||
lastUpdateRef.current = { threadId, valuesHash };
|
||||
updateThreadFromStream(threadId, streamValue.values);
|
||||
}
|
||||
}
|
||||
}, [threadId, streamValue.values, updateThreadFromStream]);
|
||||
|
||||
// Real-time updates via stream - no polling needed
|
||||
|
||||
return (
|
||||
<StreamContext.Provider value={streamValue}>
|
||||
{children}
|
||||
|
|
@ -113,12 +138,23 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
|
||||
const [isAuth, setIsAuth] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize from localStorage to prevent modal flash
|
||||
const [hasGitHubAppInstalled, setHasGitHubAppInstalled] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
>(() => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const cached = localStorage.getItem("github_app_installed");
|
||||
return cached === "true" ? true : cached === "false" ? false : null;
|
||||
});
|
||||
|
||||
const [isCheckingAppInstallation, setIsCheckingAppInstallation] =
|
||||
useState(false);
|
||||
const { token: githubToken, fetchToken: fetchGitHubToken } = useGitHubToken();
|
||||
const {
|
||||
token: githubToken,
|
||||
fetchToken: fetchGitHubToken,
|
||||
isLoading: isTokenLoading,
|
||||
} = useGitHubToken();
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
|
|
@ -126,9 +162,24 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
checkGitHubAppInstallation();
|
||||
// Check if we already have cached installation status
|
||||
const cachedInstallationStatus = localStorage.getItem(
|
||||
"github_app_installed",
|
||||
);
|
||||
if (cachedInstallationStatus === "true") {
|
||||
setHasGitHubAppInstalled(true);
|
||||
// Fetch token if we don't have one yet
|
||||
if (!githubToken && !isTokenLoading) {
|
||||
fetchGitHubToken();
|
||||
}
|
||||
} else if (cachedInstallationStatus === "false") {
|
||||
setHasGitHubAppInstalled(false);
|
||||
} else {
|
||||
// Only check installation if we don't have cached status
|
||||
checkGitHubAppInstallation();
|
||||
}
|
||||
}
|
||||
}, [isAuth]);
|
||||
}, [isAuth, githubToken, isTokenLoading]);
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
|
|
@ -147,20 +198,24 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
const response = await fetch("/api/github/repositories");
|
||||
if (response.ok) {
|
||||
setHasGitHubAppInstalled(true);
|
||||
localStorage.setItem("github_app_installed", "true");
|
||||
// If the app is installed, fetch a token
|
||||
await fetchGitHubToken();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error.includes("installation")) {
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
} else {
|
||||
// If there's a different error, we'll assume the app is not installed
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking GitHub App installation:", error);
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
} finally {
|
||||
setIsCheckingAppInstallation(false);
|
||||
}
|
||||
|
|
@ -173,6 +228,8 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
|
||||
const handleInstallGitHubApp = () => {
|
||||
setIsLoading(true);
|
||||
// Clear cached status so we check again after installation
|
||||
localStorage.removeItem("github_app_installed");
|
||||
window.location.href = "/api/github/installation";
|
||||
};
|
||||
|
||||
|
|
@ -271,7 +328,9 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
}
|
||||
|
||||
// Step 2: GitHub App Installation (only show if authenticated but app not installed)
|
||||
if ((isAuth && hasGitHubAppInstalled === false) || !githubToken) {
|
||||
// Only show modal if we've definitively determined the app is not installed
|
||||
// Don't show while we're still loading or have cached positive status
|
||||
if (isAuth && hasGitHubAppInstalled === false && !isTokenLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex w-full max-w-3xl flex-col rounded-lg border shadow-lg">
|
||||
|
|
@ -320,6 +379,27 @@ export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
|||
);
|
||||
}
|
||||
|
||||
// Only render StreamSession when we have a valid token
|
||||
if (!githubToken) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex w-full max-w-3xl flex-col rounded-lg border shadow-lg">
|
||||
<div className="flex flex-col gap-4 border-b p-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Loading...
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Setting up your GitHub integration...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StreamSession
|
||||
apiUrl={apiUrl}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { validate } from "uuid";
|
||||
import { getApiKey } from "@/lib/api-key";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
import { useQueryState } from "nuqs";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
|
|
@ -10,15 +8,30 @@ import {
|
|||
useState,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { createClient } from "./client";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
export interface ThreadWithTasks extends Thread {
|
||||
threadTitle: string;
|
||||
repository: string;
|
||||
branch: string;
|
||||
completedTasksCount: number;
|
||||
totalTasksCount: number;
|
||||
tasks: TaskPlan | undefined;
|
||||
proposedPlan: string[];
|
||||
}
|
||||
|
||||
interface ThreadContextType {
|
||||
getThreads: () => Promise<Thread[]>;
|
||||
threads: Thread[];
|
||||
setThreads: Dispatch<SetStateAction<Thread[]>>;
|
||||
threads: ThreadWithTasks[];
|
||||
setThreads: Dispatch<SetStateAction<ThreadWithTasks[]>>;
|
||||
threadsLoading: boolean;
|
||||
setThreadsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
refreshThreads: () => Promise<void>;
|
||||
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
|
||||
updateThreadFromStream: (threadId: string, streamValues: any) => void;
|
||||
}
|
||||
|
||||
const ThreadContext = createContext<ThreadContextType | undefined>(undefined);
|
||||
|
|
@ -33,34 +46,223 @@ function getThreadSearchMetadata(
|
|||
}
|
||||
}
|
||||
|
||||
const getTaskCounts = (
|
||||
tasks?: TaskPlan,
|
||||
proposedPlan?: string[],
|
||||
): { totalTasksCount: number; completedTasksCount: number } => {
|
||||
if (proposedPlan && !tasks) {
|
||||
return {
|
||||
totalTasksCount: proposedPlan.length,
|
||||
completedTasksCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (!tasks) {
|
||||
// No tasks passed, return 0s
|
||||
return { totalTasksCount: 0, completedTasksCount: 0 };
|
||||
}
|
||||
|
||||
const activeTaskList = tasks.tasks.find(
|
||||
(t) => t.taskIndex === tasks.activeTaskIndex,
|
||||
);
|
||||
if (!activeTaskList) {
|
||||
// Something is wrong here. Return 0
|
||||
return { totalTasksCount: 0, completedTasksCount: 0 };
|
||||
}
|
||||
|
||||
const activeTaskPlans = activeTaskList.planRevisions.find(
|
||||
(p) => p.revisionIndex === activeTaskList.activeRevisionIndex,
|
||||
);
|
||||
if (!activeTaskPlans) {
|
||||
// Something is wrong here. Return 0
|
||||
return { totalTasksCount: 0, completedTasksCount: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
totalTasksCount: activeTaskPlans.plans.length,
|
||||
completedTasksCount: activeTaskPlans.plans.filter((p) => p.completed)
|
||||
.length,
|
||||
};
|
||||
};
|
||||
|
||||
export function ThreadProvider({ children }: { children: ReactNode }) {
|
||||
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||
const assistantId: string | undefined =
|
||||
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
||||
|
||||
const [threads, setThreads] = useState<Thread[]>([]);
|
||||
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
|
||||
const [threadsLoading, setThreadsLoading] = useState(false);
|
||||
|
||||
const getThreads = useCallback(async (): Promise<Thread[]> => {
|
||||
if (!apiUrl || !assistantId) return [];
|
||||
const client = createClient(apiUrl, getApiKey() ?? undefined);
|
||||
// Real-time thread updater for all properties (replaces polling)
|
||||
const updateThreadFromStream = useCallback(
|
||||
(threadId: string, streamValues: any) => {
|
||||
if (!threadId || !streamValues) return;
|
||||
|
||||
const threads = await client.threads.search({
|
||||
metadata: {
|
||||
...getThreadSearchMetadata(assistantId),
|
||||
},
|
||||
limit: 100,
|
||||
});
|
||||
setThreads((currentThreads) => {
|
||||
const targetThread = currentThreads.find(
|
||||
(t) => t.thread_id === threadId,
|
||||
);
|
||||
if (!targetThread) return currentThreads; // Thread not found, no update needed
|
||||
|
||||
return threads;
|
||||
const plan: TaskPlan | undefined = streamValues?.plan;
|
||||
const proposedPlan: string[] = streamValues?.proposedPlan || [];
|
||||
const targetRepository = streamValues?.targetRepository;
|
||||
const messages = streamValues?.messages;
|
||||
|
||||
const { totalTasksCount, completedTasksCount } = getTaskCounts(
|
||||
plan,
|
||||
proposedPlan,
|
||||
);
|
||||
|
||||
// Extract thread title from messages if available
|
||||
const firstMessageContent = messages?.[0]?.content;
|
||||
const threadTitle = firstMessageContent
|
||||
? getMessageContentString(firstMessageContent)
|
||||
: targetThread.threadTitle;
|
||||
|
||||
const newRepository =
|
||||
targetRepository?.repo ||
|
||||
targetRepository?.name ||
|
||||
targetThread.repository ||
|
||||
"Unknown Repository";
|
||||
|
||||
const newBranch =
|
||||
targetRepository?.branch || targetThread.branch || "main";
|
||||
|
||||
return currentThreads.map((thread) => {
|
||||
if (thread.thread_id === threadId) {
|
||||
return {
|
||||
...thread,
|
||||
threadTitle,
|
||||
repository: newRepository,
|
||||
branch: newBranch,
|
||||
completedTasksCount,
|
||||
totalTasksCount,
|
||||
tasks: plan,
|
||||
proposedPlan,
|
||||
};
|
||||
}
|
||||
return thread;
|
||||
});
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getThread = useCallback(
|
||||
async (threadId: string): Promise<ThreadWithTasks | null> => {
|
||||
if (!apiUrl || !assistantId) return null;
|
||||
const client = createClient(apiUrl);
|
||||
|
||||
try {
|
||||
const thread = await client.threads.get(threadId);
|
||||
return enhanceThreadWithTasks(thread);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch thread:", threadId, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[apiUrl, assistantId],
|
||||
);
|
||||
|
||||
const enhanceThreadWithTasks = (thread: Thread): ThreadWithTasks => {
|
||||
const threadValues = thread.values as any;
|
||||
const plan: TaskPlan | undefined = threadValues?.plan;
|
||||
const proposedPlan: string[] = threadValues?.proposedPlan || [];
|
||||
|
||||
const targetRepository = threadValues?.targetRepository;
|
||||
const messages = (threadValues as any)?.messages;
|
||||
const firstMessageContent = messages?.[0]?.content;
|
||||
const threadTitle = firstMessageContent
|
||||
? getMessageContentString(firstMessageContent)
|
||||
: `Thread ${thread.thread_id.substring(0, 8)}`;
|
||||
|
||||
const { totalTasksCount, completedTasksCount } = getTaskCounts(
|
||||
plan,
|
||||
proposedPlan,
|
||||
);
|
||||
|
||||
return {
|
||||
...thread,
|
||||
threadTitle,
|
||||
repository:
|
||||
targetRepository?.repo ||
|
||||
targetRepository?.name ||
|
||||
"Unknown Repository",
|
||||
branch: targetRepository?.branch || "main",
|
||||
completedTasksCount,
|
||||
totalTasksCount,
|
||||
tasks: plan,
|
||||
proposedPlan,
|
||||
};
|
||||
};
|
||||
|
||||
const refreshThreads = useCallback(async (): Promise<void> => {
|
||||
if (!apiUrl || !assistantId) return;
|
||||
|
||||
setThreadsLoading(true);
|
||||
const client = createClient(apiUrl);
|
||||
|
||||
try {
|
||||
// Simple thread search - try both metadata approaches
|
||||
const searchParams = {
|
||||
limit: 100,
|
||||
metadata: getThreadSearchMetadata(assistantId),
|
||||
};
|
||||
|
||||
let threadsResponse = await client.threads.search(searchParams);
|
||||
|
||||
// If no threads found, try alternative metadata
|
||||
if (threadsResponse.length === 0) {
|
||||
const altMetadata = assistantId.includes("-")
|
||||
? { assistant_id: assistantId }
|
||||
: { graph_id: assistantId };
|
||||
threadsResponse = await client.threads.search({
|
||||
limit: 100,
|
||||
metadata: altMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
// Enhance threads with task data
|
||||
const enhancedThreads: ThreadWithTasks[] = [];
|
||||
for (const thread of threadsResponse) {
|
||||
try {
|
||||
const fullThread = await client.threads.get(thread.thread_id);
|
||||
enhancedThreads.push(enhanceThreadWithTasks(fullThread));
|
||||
} catch (error) {
|
||||
console.error(`Failed to enhance thread ${thread.thread_id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
enhancedThreads.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
|
||||
setThreads(enhancedThreads);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch threads:", error);
|
||||
} finally {
|
||||
setThreadsLoading(false);
|
||||
}
|
||||
}, [apiUrl, assistantId]);
|
||||
|
||||
// Removed polling - now using real-time stream updates via updateThreadFromStream
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
refreshThreads();
|
||||
}, [refreshThreads]);
|
||||
|
||||
const value = {
|
||||
getThreads,
|
||||
threads,
|
||||
setThreads,
|
||||
threadsLoading,
|
||||
setThreadsLoading,
|
||||
refreshThreads,
|
||||
getThread,
|
||||
updateThreadFromStream,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
export function createClient(apiUrl: string, apiKey: string | undefined) {
|
||||
export function createClient(apiUrl: string) {
|
||||
return new Client({
|
||||
apiKey,
|
||||
apiUrl,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
7
apps/web/src/types/file-upload.ts
Normal file
7
apps/web/src/types/file-upload.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const SUPPORTED_FILE_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"application/pdf",
|
||||
];
|
||||
50
apps/web/src/types/index.ts
Normal file
50
apps/web/src/types/index.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Dispatch, SetStateAction } from "react";
|
||||
import { ThreadStatus } from "@langchain/langgraph-sdk";
|
||||
import { TaskPlan } from "@open-swe/shared/open-swe/types";
|
||||
|
||||
// Task with status extended from the agent
|
||||
export interface TaskWithStatus extends TaskPlan {
|
||||
status: ThreadStatus;
|
||||
repository?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
// Enhanced task type with thread context
|
||||
export interface TaskWithContext extends TaskWithStatus {
|
||||
taskId: string; // Globally unique UUID
|
||||
threadId: string; // Internal reference (not exposed to user)
|
||||
threadTitle?: string;
|
||||
branch?: string;
|
||||
createdAt: string; // For chronological sorting
|
||||
// status, repository, and date are inherited from TaskWithStatus
|
||||
}
|
||||
|
||||
export interface TaskContextType {
|
||||
getTasks: (threadId: string) => Promise<TaskWithStatus[]>;
|
||||
getAllTasks: () => Promise<TaskWithContext[]>;
|
||||
refreshStatus: () => Promise<void>;
|
||||
tasks: TaskWithStatus[];
|
||||
setTasks: Dispatch<SetStateAction<TaskWithStatus[]>>;
|
||||
allTasks: TaskWithContext[];
|
||||
setAllTasks: Dispatch<SetStateAction<TaskWithContext[]>>;
|
||||
tasksLoading: boolean;
|
||||
setTasksLoading: Dispatch<SetStateAction<boolean>>;
|
||||
// Active thread management for real-time status updates
|
||||
addActiveThread: (threadId: string) => void;
|
||||
removeActiveThread: (threadId: string) => void;
|
||||
activeThreads: Set<string>;
|
||||
}
|
||||
|
||||
// Thread summary for grouping tasks
|
||||
export interface ThreadSummary {
|
||||
threadId: string;
|
||||
threadTitle: string;
|
||||
repository: string;
|
||||
branch: string;
|
||||
date: string;
|
||||
createdAt: string;
|
||||
tasks: TaskWithContext[];
|
||||
completedTasksCount: number;
|
||||
totalTasksCount: number;
|
||||
status: ThreadStatus;
|
||||
}
|
||||
|
|
@ -76,6 +76,85 @@ export async function getInstallationRepositories(
|
|||
return data.repositories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches branches for a specific repository using OAuth access token
|
||||
*/
|
||||
export async function getRepositoryBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
accessToken: string,
|
||||
): Promise<Branch[]> {
|
||||
const allBranches: Branch[] = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // Maximum allowed by GitHub API
|
||||
|
||||
// First, get repository info to ensure we have the default branch
|
||||
const repoResponse = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let defaultBranch: string | null = null;
|
||||
if (repoResponse.ok) {
|
||||
const repoData = await repoResponse.json();
|
||||
defaultBranch = repoData.default_branch;
|
||||
}
|
||||
|
||||
// Fetch all branches with pagination
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allBranches.push(...data);
|
||||
|
||||
// If we got less than the requested amount, we've reached the end
|
||||
if (data.length < perPage) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
// Ensure default branch is at the beginning if it exists
|
||||
if (defaultBranch) {
|
||||
const defaultBranchIndex = allBranches.findIndex(
|
||||
(branch) => branch.name === defaultBranch,
|
||||
);
|
||||
if (defaultBranchIndex > 0) {
|
||||
const defaultBranchData = allBranches[defaultBranchIndex];
|
||||
allBranches.splice(defaultBranchIndex, 1);
|
||||
allBranches.unshift(defaultBranchData);
|
||||
}
|
||||
}
|
||||
|
||||
return allBranches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository interface representing GitHub repository data
|
||||
*/
|
||||
|
|
@ -95,3 +174,15 @@ export interface Repository {
|
|||
pull: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Branch interface representing GitHub branch data
|
||||
*/
|
||||
export interface Branch {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
url: string;
|
||||
};
|
||||
protected: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"open-swe": "./apps/open-swe/src/index.ts:graph"
|
||||
"open_swe": "./apps/open-swe/src/index.ts:graph"
|
||||
},
|
||||
"env": "./apps/open-swe/.env",
|
||||
"dependencies": ["./apps/open-swe"]
|
||||
|
|
|
|||
|
|
@ -19,5 +19,8 @@
|
|||
"turbo": "^2.5.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@langchain/langgraph-sdk": "^0.0.83"
|
||||
},
|
||||
"packageManager": "yarn@3.5.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.56",
|
||||
"@langchain/langgraph": "^0.2.73",
|
||||
"@langchain/langgraph": "^0.3.1",
|
||||
"@langchain/langgraph-sdk": "^0.0.83",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
69
packages/shared/src/configurable-metadata.ts
Normal file
69
packages/shared/src/configurable-metadata.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// The type interface for configuration fields
|
||||
|
||||
export type ConfigurableFieldUIType =
|
||||
| "text"
|
||||
| "textarea"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "slider"
|
||||
| "select"
|
||||
| "json";
|
||||
|
||||
/**
|
||||
* The type interface for options in a select field.
|
||||
*/
|
||||
export interface ConfigurableFieldOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI configuration for a field in the configurable object.
|
||||
*/
|
||||
export type ConfigurableFieldUIMetadata = {
|
||||
/**
|
||||
* The label of the field. This will be what is rendered in the UI.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The default value to render in the UI component.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
default?: unknown;
|
||||
/**
|
||||
* The type of the field.
|
||||
* @default "text"
|
||||
*/
|
||||
type?: ConfigurableFieldUIType;
|
||||
/**
|
||||
* The description of the field. This will be rendered below the UI component.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The placeholder of the field. This will be rendered inside the UI component.
|
||||
* This is only applicable for text, textarea, number, json, and select fields.
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* The options of the field. These will be the options rendered in the select UI component.
|
||||
* This is only applicable for select fields.
|
||||
*/
|
||||
options?: ConfigurableFieldOption[];
|
||||
/**
|
||||
* The minimum value of the field.
|
||||
* This is only applicable for number fields.
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* The maximum value of the field.
|
||||
* This is only applicable for number fields.
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* The step value of the field. E.g if using a slider, where you want
|
||||
* people to be able to increment by 0.1, you would set this field to 0.1
|
||||
* This is only applicable for number fields.
|
||||
*/
|
||||
step?: number;
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export const TIMEOUT_SEC = 60; // 1 minute
|
||||
export const SANDBOX_ROOT_DIR = "/home/daytona";
|
||||
export const SNAPSHOT_NAME = "daytonaio/langchain-open-swe:0.1.0";
|
||||
export const PLAN_INTERRUPT_DELIMITER = ":::";
|
||||
export const PLAN_INTERRUPT_ACTION_TITLE = "Approve/Edit Plan";
|
||||
|
|
|
|||
10
packages/shared/src/messages.ts
Normal file
10
packages/shared/src/messages.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { MessageContent } from "@langchain/core/messages";
|
||||
|
||||
export function getMessageContentString(content: MessageContent): string {
|
||||
if (typeof content === "string") return content;
|
||||
|
||||
return content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join(" ");
|
||||
}
|
||||
63
packages/shared/src/open-swe/models.ts
Normal file
63
packages/shared/src/open-swe/models.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
export const MODEL_OPTIONS = [
|
||||
{
|
||||
label: "Claude Sonnet 4 (Extended Thinking)",
|
||||
value: "anthropic:extended-thinking:claude-sonnet-4-0",
|
||||
},
|
||||
{
|
||||
label: "Claude Opus 4 (Extended Thinking)",
|
||||
value: "anthropic:extended-thinking:claude-opus-4-0",
|
||||
},
|
||||
{
|
||||
label: "Claude Sonnet 4",
|
||||
value: "anthropic:claude-sonnet-4-0",
|
||||
},
|
||||
{
|
||||
label: "Claude Opus 4",
|
||||
value: "anthropic:claude-opus-4-0",
|
||||
},
|
||||
{
|
||||
label: "Claude 3.7 Sonnet",
|
||||
value: "anthropic:claude-3-7-sonnet-latest",
|
||||
},
|
||||
{
|
||||
label: "Claude 3.5 Sonnet",
|
||||
value: "anthropic:claude-3-5-sonnet-latest",
|
||||
},
|
||||
{
|
||||
label: "o4",
|
||||
value: "openai:o4",
|
||||
},
|
||||
{
|
||||
label: "o4 mini",
|
||||
value: "openai:o4-mini",
|
||||
},
|
||||
{
|
||||
label: "o3",
|
||||
value: "openai:o3",
|
||||
},
|
||||
{
|
||||
label: "o3 mini",
|
||||
value: "openai:o3-mini",
|
||||
},
|
||||
{
|
||||
label: "GPT 4o",
|
||||
value: "openai:gpt-4o",
|
||||
},
|
||||
{
|
||||
label: "GPT 4.1",
|
||||
value: "openai:gpt-4.1",
|
||||
},
|
||||
{
|
||||
label: "Gemini 2.5 Pro Preview",
|
||||
value: "google-genai:gemini-2.5-pro-preview-05-06",
|
||||
},
|
||||
{
|
||||
label: "Gemini 2.5 Flash Preview",
|
||||
value: "google-genai:gemini-2.5-flash-preview-05-20",
|
||||
},
|
||||
];
|
||||
|
||||
export const MODEL_OPTIONS_NO_THINKING = MODEL_OPTIONS.filter(
|
||||
({ value }) =>
|
||||
!value.includes("extended-thinking") || !value.startsWith("openai:o"),
|
||||
);
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import "@langchain/langgraph/zod";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
addMessages,
|
||||
LangGraphRunnableConfig,
|
||||
Messages,
|
||||
} from "@langchain/langgraph";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
MessagesZodState,
|
||||
} from "@langchain/langgraph/web";
|
||||
import { MODEL_OPTIONS, MODEL_OPTIONS_NO_THINKING } from "./models.js";
|
||||
import { ConfigurableFieldUIMetadata } from "../configurable-metadata.js";
|
||||
import {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
export type PlanItem = {
|
||||
/**
|
||||
|
|
@ -108,11 +113,7 @@ export type TargetRepository = {
|
|||
branch?: string;
|
||||
};
|
||||
|
||||
export const GraphAnnotation = z.object({
|
||||
messages: z
|
||||
.custom<BaseMessage[]>()
|
||||
.default(() => [])
|
||||
.langgraph.reducer<Messages>((state, update) => addMessages(state, update)),
|
||||
export const GraphAnnotation = MessagesZodState.extend({
|
||||
proposedPlan: z
|
||||
.array(z.string())
|
||||
.default(() => [])
|
||||
|
|
@ -153,74 +154,147 @@ export const GraphAnnotation = z.object({
|
|||
.string()
|
||||
.default(() => "")
|
||||
.langgraph.reducer((_state, update) => update),
|
||||
ui: z
|
||||
.custom<UIMessage[]>()
|
||||
.default(() => [])
|
||||
.langgraph.reducer<(UIMessage | RemoveUIMessage)[]>((state, update) =>
|
||||
uiMessageReducer(state, update),
|
||||
),
|
||||
// TODO: Not used, but can be used in the future for Gen UI artifacts
|
||||
context: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
export type GraphState = z.infer<typeof GraphAnnotation>;
|
||||
export type GraphUpdate = Partial<GraphState>;
|
||||
|
||||
const MODEL_OPTIONS = [
|
||||
{
|
||||
label: "Claude Sonnet 4 (Extended Thinking)",
|
||||
value: "anthropic:extended-thinking:claude-sonnet-4-0",
|
||||
export const GraphConfigurationMetadata: {
|
||||
[key: string]: {
|
||||
x_open_swe_ui_config:
|
||||
| Omit<ConfigurableFieldUIMetadata, "label">
|
||||
| { type: "hidden" };
|
||||
};
|
||||
} = {
|
||||
plannerModelName: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for planning",
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Claude Opus 4 (Extended Thinking)",
|
||||
value: "anthropic:extended-thinking:claude-opus-4-0",
|
||||
plannerTemperature: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Claude Sonnet 4",
|
||||
value: "anthropic:claude-sonnet-4-0",
|
||||
plannerContextModelName: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for planning",
|
||||
options: MODEL_OPTIONS,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Claude Opus 4",
|
||||
value: "anthropic:claude-opus-4-0",
|
||||
plannerContextTemperature: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Claude 3.7 Sonnet",
|
||||
value: "anthropic:claude-3-7-sonnet-latest",
|
||||
actionGeneratorModelName: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for action generation",
|
||||
options: MODEL_OPTIONS,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Claude 3.5 Sonnet",
|
||||
value: "anthropic:claude-3-5-sonnet-latest",
|
||||
actionGeneratorTemperature: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "o4",
|
||||
value: "openai:o4",
|
||||
progressPlanCheckerModelName: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for progress plan checking",
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "o4 mini",
|
||||
value: "openai:o4-mini",
|
||||
progressPlanCheckerTemperature: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "o3",
|
||||
value: "openai:o3",
|
||||
summarizerModelName: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for summarizing the conversation history",
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "o3 mini",
|
||||
value: "openai:o3-mini",
|
||||
summarizerTemperature: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "GPT 4o",
|
||||
value: "openai:gpt-4o",
|
||||
maxContextActions: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "number",
|
||||
default: 6,
|
||||
min: 1,
|
||||
max: 20,
|
||||
description:
|
||||
"Maximum number of context gathering actions during planning",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "GPT 4.1",
|
||||
value: "openai:gpt-4.1",
|
||||
maxTokens: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "number",
|
||||
default: 10_000,
|
||||
min: 1,
|
||||
max: 64_000,
|
||||
description:
|
||||
"The maximum number of tokens to generate in an individual generation",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Gemini 2.5 Pro Preview",
|
||||
value: "google-genai:gemini-2.5-pro-preview-05-06",
|
||||
"x-github-installation-token": {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Gemini 2.5 Flash Preview",
|
||||
value: "google-genai:gemini-2.5-flash-preview-05-20",
|
||||
"x-github-access-token": {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const MODEL_OPTIONS_NO_THINKING = MODEL_OPTIONS.filter(
|
||||
({ value }) =>
|
||||
!value.includes("extended-thinking") || !value.startsWith("openai:o"),
|
||||
);
|
||||
};
|
||||
|
||||
export const GraphConfiguration = z.object({
|
||||
/**
|
||||
|
|
@ -231,15 +305,7 @@ export const GraphConfiguration = z.object({
|
|||
plannerModelName: z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for planning",
|
||||
// Do not show extended thinking models
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.plannerModelName),
|
||||
/**
|
||||
* The temperature to use for the planning step.
|
||||
* This includes initial planning, and rewriting.
|
||||
|
|
@ -249,16 +315,7 @@ export const GraphConfiguration = z.object({
|
|||
plannerTemperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.plannerTemperature),
|
||||
/**
|
||||
* The model ID to use for the planning step.
|
||||
* This includes initial planning, and rewriting.
|
||||
|
|
@ -267,14 +324,7 @@ export const GraphConfiguration = z.object({
|
|||
plannerContextModelName: z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for planning",
|
||||
options: MODEL_OPTIONS,
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.plannerContextModelName),
|
||||
/**
|
||||
* The temperature to use for the planning step.
|
||||
* This includes initial planning, and rewriting.
|
||||
|
|
@ -284,16 +334,7 @@ export const GraphConfiguration = z.object({
|
|||
plannerContextTemperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.plannerContextTemperature),
|
||||
|
||||
/**
|
||||
* The model ID to use for action generation.
|
||||
|
|
@ -302,14 +343,7 @@ export const GraphConfiguration = z.object({
|
|||
actionGeneratorModelName: z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for action generation",
|
||||
options: MODEL_OPTIONS,
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.actionGeneratorModelName),
|
||||
/**
|
||||
* The temperature to use for action generation.
|
||||
* If selecting a reasoning model, this will be ignored.
|
||||
|
|
@ -318,16 +352,7 @@ export const GraphConfiguration = z.object({
|
|||
actionGeneratorTemperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.actionGeneratorTemperature),
|
||||
|
||||
/**
|
||||
* The model ID to use for progress plan checking.
|
||||
|
|
@ -336,14 +361,9 @@ export const GraphConfiguration = z.object({
|
|||
progressPlanCheckerModelName: z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description: "The model to use for progress plan checking",
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(
|
||||
GraphConfigurationMetadata.progressPlanCheckerModelName,
|
||||
),
|
||||
/**
|
||||
* The temperature to use for progress plan checking.
|
||||
* If selecting a reasoning model, this will be ignored.
|
||||
|
|
@ -352,16 +372,9 @@ export const GraphConfiguration = z.object({
|
|||
progressPlanCheckerTemperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(
|
||||
GraphConfigurationMetadata.progressPlanCheckerTemperature,
|
||||
),
|
||||
|
||||
/**
|
||||
* The model ID to use for summarizing the conversation history.
|
||||
|
|
@ -370,15 +383,7 @@ export const GraphConfiguration = z.object({
|
|||
summarizerModelName: z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "select",
|
||||
default: "anthropic:claude-sonnet-4-0",
|
||||
description:
|
||||
"The model to use for summarizing the conversation history",
|
||||
options: MODEL_OPTIONS_NO_THINKING,
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.summarizerModelName),
|
||||
/**
|
||||
* The temperature to use for summarizing the conversation history.
|
||||
* If selecting a reasoning model, this will be ignored.
|
||||
|
|
@ -387,16 +392,7 @@ export const GraphConfiguration = z.object({
|
|||
summarizerTemperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "slider",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
description: "Controls randomness (0 = deterministic, 2 = creative)",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.summarizerTemperature),
|
||||
|
||||
/**
|
||||
* The maximum number of context gathering actions to take during planning.
|
||||
|
|
@ -407,16 +403,7 @@ export const GraphConfiguration = z.object({
|
|||
maxContextActions: z
|
||||
.number()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "number",
|
||||
default: 6,
|
||||
min: 1,
|
||||
max: 20,
|
||||
description:
|
||||
"Maximum number of context gathering actions during planning",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.maxContextActions),
|
||||
/**
|
||||
* The maximum number of tokens to generate in an individual generation.
|
||||
* @default 10_000
|
||||
|
|
@ -425,38 +412,23 @@ export const GraphConfiguration = z.object({
|
|||
.number()
|
||||
.optional()
|
||||
.default(() => 10_000)
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "number",
|
||||
default: 10_000,
|
||||
min: 1,
|
||||
max: 64_000,
|
||||
description:
|
||||
"The maximum number of tokens to generate in an individual generation",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata.maxTokens),
|
||||
/**
|
||||
* The user's GitHub installation token. To be used to take actions on behalf of the user.
|
||||
*/
|
||||
"x-github-installation-token": z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(
|
||||
GraphConfigurationMetadata["x-github-installation-token"],
|
||||
),
|
||||
/**
|
||||
* The user's GitHub access token. To be used in requests to get information about the user.
|
||||
*/
|
||||
"x-github-access-token": z
|
||||
.string()
|
||||
.optional()
|
||||
.langgraph.metadata({
|
||||
x_oap_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
}),
|
||||
.langgraph.metadata(GraphConfigurationMetadata["x-github-access-token"]),
|
||||
});
|
||||
|
||||
export type GraphConfig = LangGraphRunnableConfig<
|
||||
|
|
|
|||
230
yarn.lock
230
yarn.lock
|
|
@ -1488,14 +1488,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@langchain/langgraph-checkpoint@npm:~0.0.17":
|
||||
version: 0.0.17
|
||||
resolution: "@langchain/langgraph-checkpoint@npm:0.0.17"
|
||||
"@langchain/langgraph-checkpoint@npm:~0.0.18":
|
||||
version: 0.0.18
|
||||
resolution: "@langchain/langgraph-checkpoint@npm:0.0.18"
|
||||
dependencies:
|
||||
uuid: ^10.0.0
|
||||
peerDependencies:
|
||||
"@langchain/core": ">=0.2.31 <0.4.0"
|
||||
checksum: c8dc429e66ff5b0f31037c493c4f8687196f1800d52b48e8ff04829742cd7200af66ce46dee93ff3c0f6b6e6c2af36336f09626a5ce296ec610383a3bd7131ef
|
||||
checksum: bd7ba56696bbfde0be8f2cef2f0dadf30826732cbb1d9bf92439f314a01219053c50ff6b624ef58a9d6723d6e1a8b872e725abc4d95e4ea9610a41c5f1e41047
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -1527,9 +1527,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@langchain/langgraph-sdk@npm:^0.0.78, @langchain/langgraph-sdk@npm:~0.0.32":
|
||||
version: 0.0.78
|
||||
resolution: "@langchain/langgraph-sdk@npm:0.0.78"
|
||||
"@langchain/langgraph-sdk@npm:^0.0.83":
|
||||
version: 0.0.83
|
||||
resolution: "@langchain/langgraph-sdk@npm:0.0.83"
|
||||
dependencies:
|
||||
"@types/json-schema": ^7.0.15
|
||||
p-queue: ^6.6.2
|
||||
|
|
@ -1543,7 +1543,7 @@ __metadata:
|
|||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
checksum: 3f391c79cf32aebe506e56122f7422f7075a0ba8d1331cde6d65de241a14b9f4ec6187baec69ea9372fa41426eb97067067c07566483b7bf2445689420238da5
|
||||
checksum: c6d037b8b9486456a8180d892e8b05ad3c2332456d7d237597720ebe42dd6b126f1aed963a0d2a681405724b7b8e0eac0607994b8e527d2f0e756e4c9e30c19e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -1562,11 +1562,11 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@langchain/langgraph@npm:^0.2.73":
|
||||
version: 0.2.73
|
||||
resolution: "@langchain/langgraph@npm:0.2.73"
|
||||
"@langchain/langgraph@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "@langchain/langgraph@npm:0.3.1"
|
||||
dependencies:
|
||||
"@langchain/langgraph-checkpoint": ~0.0.17
|
||||
"@langchain/langgraph-checkpoint": ~0.0.18
|
||||
"@langchain/langgraph-sdk": ~0.0.32
|
||||
uuid: ^10.0.0
|
||||
zod: ^3.23.8
|
||||
|
|
@ -1576,7 +1576,7 @@ __metadata:
|
|||
peerDependenciesMeta:
|
||||
zod-to-json-schema:
|
||||
optional: true
|
||||
checksum: dd9f2cb38fe6702264657ab35c60fdc7cd4e88574e3ab5a21cf18f39a4df069fef3aa9503ec33780657b8ca17c359f50e2f7247c57350306f58ee14adde400d2
|
||||
checksum: 25f4a338635c00a0bb56be483438eef29a133b3900cec91b0a32e880ba0b6ee719ea0365f957cc7a5c25218f50f10b83975f2198565aafc27d17127fabf188cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -1913,9 +1913,9 @@ __metadata:
|
|||
"@langchain/anthropic": ^0.3.20
|
||||
"@langchain/core": ^0.3.56
|
||||
"@langchain/google-genai": ^0.2.9
|
||||
"@langchain/langgraph": ^0.2.73
|
||||
"@langchain/langgraph": ^0.3.1
|
||||
"@langchain/langgraph-cli": latest
|
||||
"@langchain/langgraph-sdk": ^0.0.78
|
||||
"@langchain/langgraph-sdk": ^0.0.83
|
||||
"@langchain/openai": ^0.5.10
|
||||
"@octokit/rest": ^22.0.0
|
||||
"@open-swe/shared": "workspace:*"
|
||||
|
|
@ -1948,7 +1948,8 @@ __metadata:
|
|||
"@eslint/eslintrc": ^3.1.0
|
||||
"@eslint/js": ^9.19.0
|
||||
"@langchain/core": ^0.3.56
|
||||
"@langchain/langgraph": ^0.2.73
|
||||
"@langchain/langgraph": ^0.3.1
|
||||
"@langchain/langgraph-sdk": ^0.0.83
|
||||
"@tsconfig/recommended": ^1.0.8
|
||||
"@types/node": ^22.13.5
|
||||
dotenv: ^16.4.7
|
||||
|
|
@ -1970,16 +1971,22 @@ __metadata:
|
|||
dependencies:
|
||||
"@eslint/js": ^9.19.0
|
||||
"@langchain/core": ^0.3.57
|
||||
"@langchain/langgraph": ^0.2.73
|
||||
"@langchain/langgraph-sdk": ^0.0.78
|
||||
"@langchain/langgraph": ^0.3.1
|
||||
"@langchain/langgraph-sdk": ^0.0.83
|
||||
"@open-swe/shared": "workspace:*"
|
||||
"@radix-ui/react-alert-dialog": ^1.1.14
|
||||
"@radix-ui/react-avatar": ^1.1.3
|
||||
"@radix-ui/react-collapsible": ^1.1.11
|
||||
"@radix-ui/react-dialog": ^1.1.14
|
||||
"@radix-ui/react-label": ^2.1.2
|
||||
"@radix-ui/react-popover": ^1.1.14
|
||||
"@radix-ui/react-scroll-area": ^1.2.9
|
||||
"@radix-ui/react-select": ^2.2.5
|
||||
"@radix-ui/react-separator": ^1.1.2
|
||||
"@radix-ui/react-slot": ^1.1.2
|
||||
"@radix-ui/react-slider": ^1.3.5
|
||||
"@radix-ui/react-slot": ^1.2.3
|
||||
"@radix-ui/react-switch": ^1.1.3
|
||||
"@radix-ui/react-tabs": ^1.1.12
|
||||
"@radix-ui/react-tooltip": ^1.1.8
|
||||
"@tailwindcss/postcss": ^4.0.13
|
||||
"@types/jsonwebtoken": ^9.0.9
|
||||
|
|
@ -2032,6 +2039,7 @@ __metadata:
|
|||
use-stick-to-bottom: ^1.0.46
|
||||
uuid: ^11.1.0
|
||||
zod: ^3.24.2
|
||||
zustand: ^5.0.5
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
@ -2056,6 +2064,30 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-alert-dialog@npm:^1.1.14":
|
||||
version: 1.1.14
|
||||
resolution: "@radix-ui/react-alert-dialog@npm:1.1.14"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-compose-refs": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-dialog": 1.1.14
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-slot": 1.2.3
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 5b3fec8fc11267c6c5bcf6afcbcb5ca7e8050401c692019097d08502a43d9fd555a18beb5b74806f301074150b6212dc7523722311e8dad9baa50a502d8b3828
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-arrow@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@radix-ui/react-arrow@npm:1.1.7"
|
||||
|
|
@ -2098,6 +2130,32 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collapsible@npm:^1.1.11":
|
||||
version: 1.1.11
|
||||
resolution: "@radix-ui/react-collapsible@npm:1.1.11"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-compose-refs": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-id": 1.1.1
|
||||
"@radix-ui/react-presence": 1.1.4
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-use-controllable-state": 1.2.2
|
||||
"@radix-ui/react-use-layout-effect": 1.1.1
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: a7ef7992caa3c02952dd8210493c1807739a8ac2ffb6ecf9b07974de65ed49a93b743e325e2dcd23318ea658328a01f750ecd3a965db68059a84b14988892dc4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collection@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@radix-ui/react-collection@npm:1.1.7"
|
||||
|
|
@ -2146,7 +2204,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dialog@npm:^1.1.14, @radix-ui/react-dialog@npm:^1.1.6":
|
||||
"@radix-ui/react-dialog@npm:1.1.14, @radix-ui/react-dialog@npm:^1.1.14, @radix-ui/react-dialog@npm:^1.1.6":
|
||||
version: 1.1.14
|
||||
resolution: "@radix-ui/react-dialog@npm:1.1.14"
|
||||
dependencies:
|
||||
|
|
@ -2402,6 +2460,60 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-roving-focus@npm:1.1.10":
|
||||
version: 1.1.10
|
||||
resolution: "@radix-ui/react-roving-focus@npm:1.1.10"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-collection": 1.1.7
|
||||
"@radix-ui/react-compose-refs": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-direction": 1.1.1
|
||||
"@radix-ui/react-id": 1.1.1
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-use-callback-ref": 1.1.1
|
||||
"@radix-ui/react-use-controllable-state": 1.2.2
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 9f6afabc82e3a018cbb48f4fc94ce046f776f894df2d66c96f6b78bf864d59d71f749406e30c4600f6415a6f703c66fdbb87b2841cfa919f10d1f6602df2a5b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-scroll-area@npm:^1.2.9":
|
||||
version: 1.2.9
|
||||
resolution: "@radix-ui/react-scroll-area@npm:1.2.9"
|
||||
dependencies:
|
||||
"@radix-ui/number": 1.1.1
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-compose-refs": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-direction": 1.1.1
|
||||
"@radix-ui/react-presence": 1.1.4
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-use-callback-ref": 1.1.1
|
||||
"@radix-ui/react-use-layout-effect": 1.1.1
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: dd5e097b58bf39c51d748043e7524414da860fc116ef9265daaed7d08d5faf5f0a7b3c2acd00463f9d72d7da0eb28ce00c0b886402b87535c0c0983aad2e69b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-select@npm:^2.2.5":
|
||||
version: 2.2.5
|
||||
resolution: "@radix-ui/react-select@npm:2.2.5"
|
||||
|
|
@ -2460,7 +2572,36 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.1.2":
|
||||
"@radix-ui/react-slider@npm:^1.3.5":
|
||||
version: 1.3.5
|
||||
resolution: "@radix-ui/react-slider@npm:1.3.5"
|
||||
dependencies:
|
||||
"@radix-ui/number": 1.1.1
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-collection": 1.1.7
|
||||
"@radix-ui/react-compose-refs": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-direction": 1.1.1
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-use-controllable-state": 1.2.2
|
||||
"@radix-ui/react-use-layout-effect": 1.1.1
|
||||
"@radix-ui/react-use-previous": 1.1.1
|
||||
"@radix-ui/react-use-size": 1.1.1
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: c09071d637309ae0a4438849cca98049d9b2220f10afe53f85c8c33fe41ba8ca813854a659ecef868e48e89ac389fb821726a766fc0c6c10aa6d7d408f4485f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@radix-ui/react-slot@npm:1.2.3"
|
||||
dependencies:
|
||||
|
|
@ -2500,6 +2641,32 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-tabs@npm:^1.1.12":
|
||||
version: 1.1.12
|
||||
resolution: "@radix-ui/react-tabs@npm:1.1.12"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": 1.1.2
|
||||
"@radix-ui/react-context": 1.1.2
|
||||
"@radix-ui/react-direction": 1.1.1
|
||||
"@radix-ui/react-id": 1.1.1
|
||||
"@radix-ui/react-presence": 1.1.4
|
||||
"@radix-ui/react-primitive": 2.1.3
|
||||
"@radix-ui/react-roving-focus": 1.1.10
|
||||
"@radix-ui/react-use-controllable-state": 1.2.2
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: eb5eecb4813b784c6f324a947b1f391b1b890bc3f5bd0eb2d08e73f84a10b25bbf482a8dd9785de8a6fe49ff99860c07cf0a6953c9f9631202e5d9c8ebc06758
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-tooltip@npm:^1.1.8":
|
||||
version: 1.2.7
|
||||
resolution: "@radix-ui/react-tooltip@npm:1.2.7"
|
||||
|
|
@ -12910,6 +13077,27 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^5.0.5":
|
||||
version: 5.0.5
|
||||
resolution: "zustand@npm:5.0.5"
|
||||
peerDependencies:
|
||||
"@types/react": ">=18.0.0"
|
||||
immer: ">=9.0.6"
|
||||
react: ">=18.0.0"
|
||||
use-sync-external-store: ">=1.2.0"
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
checksum: 9f4fa5d7152c6b5a9cbcccb08ae34500c7495f8707549e7a8de3cea2844a75a81b4acc69a85601dbe539ee91f69c2d25d4e5286eb973bc76292a2012256061cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zwitch@npm:^2.0.0":
|
||||
version: 2.0.4
|
||||
resolution: "zwitch@npm:2.0.4"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue