Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ cd agent-inbox
yarn install
```

Start up the web server:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sent this in a separate PR earlier


```bash
yarn dev
```

## Configuration

Once up and running, you'll need to take two actions so that the Agent Inbox can connect to your LangGraph deployment.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"react-json-view": "^1.21.3",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.9",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
Expand Down
39 changes: 39 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,26 @@ a {
body {
@apply bg-background text-foreground;
}

/* Reintroduce some native element spacing & list styles while keeping Tailwind utilities.
This softens the effect of Preflight without disabling it entirely. */
h1, h2, h3, h4, h5, h6,
p,
ul, ol,
blockquote {
margin: revert; /* Restore default top/bottom margins */
}

ul, ol {
padding: revert; /* Restore left padding */
list-style: revert; /* Bring back bullets / numbers */
}

blockquote {
all: revert; /* Restore default quotation styling */
font-style: italic; /* Keep a bit of emphasis */
padding: 0 1rem; /* Gentle horizontal inset */
}
}

/* Change highlight color */
Expand Down Expand Up @@ -185,3 +205,22 @@ a {
tr {
height: var(--row-height);
}

/* Global table borders */
@layer base {
table {
border-collapse: collapse; /* Ensure single borders */
width: 100%;
border: 1px solid hsl(var(--border));
}
th, td {
border: 1px solid hsl(var(--border));
padding: 0.5rem 0.75rem;
vertical-align: top;
}
th {
background: hsl(var(--muted));
font-weight: 600;
text-align: left;
}
}
131 changes: 104 additions & 27 deletions src/components/agent-inbox/components/inbox-item-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SubmitType,
} from "../types";
import { Textarea } from "@/components/ui/textarea";
import { TagInput } from "@/components/ui/tag-input";
import React from "react";
import { haveArgsChanged, prettifyText } from "../utils";
import { MarkdownText } from "@/components/ui/markdown-text";
Expand Down Expand Up @@ -45,7 +46,7 @@ function ArgsRenderer({ args }: { args: Record<string, any> }) {
{prettifyText(k)}:
</p>
<span className="text-[13px] leading-[18px] text-black bg-zinc-100 rounded-xl p-3 w-full max-w-full">
<MarkdownText className="text-wrap break-all break-words whitespace-pre-wrap">
<MarkdownText className="text-wrap break-all break-words">
{value}
</MarkdownText>
</span>
Expand Down Expand Up @@ -136,7 +137,8 @@ function ResponseComponent({
value={res.args}
onChange={(e) => onResponseChange(e.target.value, res)}
onKeyDown={handleKeyDown}
rows={4}
minRows={4}
maxRows={20}
placeholder="Your response here..."
/>
</div>
Expand Down Expand Up @@ -200,7 +202,19 @@ function EditAndOrAcceptComponent({
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent
) => Promise<void>;
}) {
// Legacy sizing ref no longer needed with autosizing textarea, retained if future per-field control desired
const defaultRows = React.useRef<Record<string, number>>({});
// Capture original boolean keys from the interrupt so we can keep rendering radios
const originalBooleanKeys = React.useMemo(() => {
const keys: Record<string, true> = {};
const args = interruptValue?.action_request?.args || {};
Object.entries(args).forEach(([k, v]) => {
if (typeof v === "boolean") {
keys[k] = true;
}
});
return keys;
}, [interruptValue]);
const editResponse = humanResponse.find((r) => r.type === "edit");
const acceptResponse = humanResponse.find((r) => r.type === "accept");
if (
Expand Down Expand Up @@ -267,21 +281,73 @@ function EditAndOrAcceptComponent({
</div>

{Object.entries(editResponse.args.args).map(([k, v], idx) => {
const value = ["string", "number"].includes(typeof v)
? v
: JSON.stringify(v, null);
// 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 (
defaultRows.current[k as keyof typeof defaultRows.current] ===
undefined
) {
defaultRows.current[k as keyof typeof defaultRows.current] = !v.length
? 3
: Math.max(v.length / 30, 7);
// Determine if this key started as a boolean OR remains a primitive boolean
const isBoolean =
typeof v === "boolean" ||
(originalBooleanKeys[k] && (v === "true" || v === "false"));
// Normalize to string for consistent state shape when editing
const value = ["string", "number", "boolean"].includes(typeof v)
? String(v)
: isBoolean
? String(v)
: JSON.stringify(v, null);
const isStringArray = Array.isArray(v) && v.every((i) => typeof i === "string");
// Autosize handles vertical sizing, we keep minRows via prop on Textarea.

if (isBoolean) {
return (
<div
className="flex flex-col gap-1 items-start w-full h-full px-[1px]"
key={`allow-edit-args--${k}-${idx}`}
>
<div className="flex flex-col gap-[6px] items-start w-full">
<p className="text-sm min-w-fit font-medium">{prettifyText(k)}</p>
<div className="flex gap-4 items-center">
<label className="flex items-center gap-1 text-sm">
<input
type="radio"
name={`bool-${k}`}
value="true"
disabled={streaming}
checked={value === "true"}
onChange={() => onEditChange("true", editResponse, k)}
/>
True
</label>
<label className="flex items-center gap-1 text-sm">
<input
type="radio"
name={`bool-${k}`}
value="false"
disabled={streaming}
checked={value === "false"}
onChange={() => onEditChange("false", editResponse, k)}
/>
False
</label>
</div>
</div>
</div>
);
}

if (isStringArray) {
return (
<div
className="flex flex-col gap-1 items-start w-full h-full px-[1px]"
key={`allow-edit-args--${k}-${idx}`}
>
<div className="flex flex-col gap-[6px] items-start w-full">
<p className="text-sm min-w-fit font-medium">{prettifyText(k)}</p>
<TagInput
disabled={streaming}
value={v as string[]}
onChange={(next) => onEditChange(next, editResponse, k)}
/>
</div>
</div>
);
}
const numRows =
defaultRows.current[k as keyof typeof defaultRows.current] || 8;

return (
<div
Expand All @@ -296,7 +362,8 @@ function EditAndOrAcceptComponent({
value={value}
onChange={(e) => onEditChange(e.target.value, editResponse, k)}
onKeyDown={handleKeyDown}
rows={numRows}
minRows={2}
maxRows={18}
/>
</div>
</div>
Expand Down Expand Up @@ -346,10 +413,12 @@ export function InboxItemInput({
response: HumanResponseWithEdits,
key: string | string[]
) => {
if (
(Array.isArray(change) && !Array.isArray(key)) ||
(!Array.isArray(change) && Array.isArray(key))
) {
// Allow three shapes:
// 1. change: string, key: string – update single value
// 2. change: string[], key: string – update a single arg whose value is an array (TagInput case)
// 3. change: string[], key: string[] – batch update multiple keys (reset flow)
// Any other combination is unexpected.
if (!Array.isArray(change) && Array.isArray(key)) {
toast({
title: "Error",
description: "Something went wrong",
Expand All @@ -363,14 +432,17 @@ export function InboxItemInput({
const updatedArgs = { ...(response.args?.args || {}) };

if (Array.isArray(change) && Array.isArray(key)) {
// Handle array inputs by mapping corresponding values
// Batch update: map each change[i] to key[i]
change.forEach((value, index) => {
if (index < key.length) {
updatedArgs[key[index]] = value;
}
});
} else if (Array.isArray(change) && !Array.isArray(key)) {
// Single field holds an array value
updatedArgs[key as string] = change;
} else {
// Handle single value case
// Standard single value update
updatedArgs[key as string] = change as string;
}

Expand Down Expand Up @@ -410,10 +482,15 @@ export function InboxItemInput({
...response.args.args,
...Object.fromEntries(key.map((k, i) => [k, change[i]])),
}
: {
...response.args.args,
[key as string]: change as string,
},
: Array.isArray(change) && !Array.isArray(key)
? {
...response.args.args,
[key as string]: change,
}
: {
...response.args.args,
[key as string]: change as string,
},
},
};
if (
Expand Down
25 changes: 24 additions & 1 deletion src/components/agent-inbox/components/settings-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function SettingsPopover() {
const [langchainApiKey, setLangchainApiKey] = React.useState("");
const { getItem, setItem } = useLocalStorage();
const { getSearchParam } = useQueryParams();
const { fetchThreads } = useThreadsContext();
const { fetchThreads, autoRefreshEnabled, toggleAutoRefresh } =
useThreadsContext();
const [isRunningBackfill, setIsRunningBackfill] = React.useState(false);
const [backfillCompleted, setBackfillCompleted] = React.useState(true);
const { toast } = useToast();
Expand All @@ -51,6 +52,10 @@ export function SettingsPopover() {
}
}, [langchainApiKey]);

const handleToggleAutoRefresh = (e: React.ChangeEvent<HTMLInputElement>) => {
toggleAutoRefresh(e.target.checked);
};

const handleChangeLangChainApiKey = (
e: React.ChangeEvent<HTMLInputElement>
) => {
Expand Down Expand Up @@ -147,6 +152,24 @@ export function SettingsPopover() {
onChange={handleChangeLangChainApiKey}
/>
</div>
<div className="flex flex-col items-start gap-2 w-full border-t pt-4">
<div className="flex flex-col gap-1 w-full items-start">
<Label htmlFor="auto-refresh-toggle">Auto Refresh</Label>
<p className="text-xs text-muted-foreground">Automatically poll the selected inbox for new threads.</p>
</div>
<div className="flex items-center gap-2">
<input
id="auto-refresh-toggle"
type="checkbox"
className="h-4 w-4"
checked={autoRefreshEnabled}
onChange={handleToggleAutoRefresh}
/>
<span className="text-sm text-muted-foreground">
{autoRefreshEnabled ? "Enabled" : "Disabled"}
</span>
</div>
</div>
{!backfillCompleted && (
<div className="flex flex-col items-start gap-2 w-full border-t pt-4">
<div className="flex flex-col gap-1 w-full items-start">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function InterruptedDescriptionView({
}: InterruptedDescriptionViewProps) {
return (
<div className="pt-6 pb-2">
<MarkdownText className="text-wrap break-words whitespace-pre-wrap">
<MarkdownText className="text-wrap break-words">
{description || "No description provided"}
</MarkdownText>
</div>
Expand Down
Loading