Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b4f3c14
Redesign Shell sidebar collapse, refactor Composer, and add Portal pr…
ankit-thesys Mar 5, 2026
425f1df
Merge branch 'main' of https://github.com/ankit-thesys/openui-comp in…
ankit-thesys Mar 5, 2026
f56c123
Merge branch 'main' into feature/shell-sidebar-composer-redesign
ankit-thesys Mar 11, 2026
1617b98
Merge branch 'main' of https://github.com/ankit-thesys/openui-comp in…
ankit-thesys Mar 11, 2026
3d2df1d
Merge branch 'feature/shell-sidebar-composer-redesign' of https://git…
ankit-thesys Mar 11, 2026
dc2ff4d
removing the attachment process
ankit-thesys Mar 11, 2026
d2f7c91
Merge branch 'main' into feature/shell-sidebar-composer-redesign
ankit-thesys Mar 12, 2026
530be22
Merge branch 'main' of https://github.com/ankit-thesys/openui-comp in…
ankit-thesys Mar 16, 2026
6a3a690
feat: expose header customization props for all chat layouts
ankit-thesys Mar 16, 2026
d1d6ce9
format fix
ankit-thesys Mar 16, 2026
c458d80
auto Focus, on composer
ankit-thesys Mar 16, 2026
e91178c
pr comments, and some improvement
ankit-thesys Mar 20, 2026
a1a965d
Merge branch 'main' of https://github.com/ankit-thesys/openui-comp in…
ankit-thesys Mar 20, 2026
ad90dad
Merge branch 'main' into feature/shell-sidebar-composer-redesign
ankit-thesys Mar 20, 2026
ed57260
bug fix
ankit-thesys Mar 20, 2026
6ee4ec4
Merge branch 'feature/shell-sidebar-composer-redesign' of https://git…
ankit-thesys Mar 20, 2026
d9c70b9
script fix for index.scss and fixing index.scss
ankit-thesys Mar 20, 2026
c22efaf
ready to publish package
ankit-thesys Mar 20, 2026
55789d0
Merge branch 'main' into feature/shell-sidebar-composer-redesign
ankit-thesys Mar 20, 2026
788efc4
Merge branch 'main' of https://github.com/ankit-thesys/openui-comp in…
ankit-thesys Mar 20, 2026
a8cd8ee
correct path
ankit-thesys Mar 20, 2026
9afa07e
removing portal dependence
ankit-thesys Mar 20, 2026
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
1 change: 1 addition & 0 deletions packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-portal": "^1.1.10",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.7",
Expand Down
63 changes: 3 additions & 60 deletions packages/react-ui/src/components/BottomTray/Thread.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-headless";
import { MessageProvider, useThread } from "@openuidev/react-headless";
import clsx from "clsx";
import { ArrowUp, Square } from "lucide-react";
import React, { memo, useEffect, useLayoutEffect, useRef } from "react";
import { useComposerState } from "../../hooks/useComposerState";
import React, { memo, useEffect, useRef } from "react";
import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom";
import { IconButton } from "../IconButton";
import { MarkDownRenderer } from "../MarkDownRenderer";
import { MessageLoading as MessageLoadingComponent } from "../MessageLoading";
import type { AssistantMessageComponent, UserMessageComponent } from "../OpenUIChat/types";
Expand Down Expand Up @@ -292,59 +289,5 @@ export const Messages = ({
);
};

export const Composer = ({ className }: { className?: string }) => {
const { textContent, setTextContent } = useComposerState();
const processMessage = useThread((s) => s.processMessage);
const cancelMessage = useThread((s) => s.cancelMessage);
const isRunning = useThread((s) => s.isRunning);
const inputRef = useRef<HTMLTextAreaElement>(null);

const handleSubmit = () => {
if (!textContent.trim() || isRunning) {
return;
}

processMessage({
role: "user",
content: textContent,
});

setTextContent("");
};

useLayoutEffect(() => {
const input = inputRef.current;
if (!input) {
return;
}

input.style.height = "auto";
input.style.height = `${input.scrollHeight}px`;
}, [textContent]);

return (
<div className={clsx("openui-bottom-tray-thread-composer", className)}>
<div className="openui-bottom-tray-thread-composer__input-wrapper">
<textarea
ref={inputRef}
rows={1}
value={textContent}
autoFocus
onChange={(e) => setTextContent(e.target.value)}
className="openui-bottom-tray-thread-composer__input"
placeholder="Type your message..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}}
/>
<IconButton
onClick={isRunning ? cancelMessage : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowUp size="1em" />}
/>
</div>
</div>
);
};
// Re-export Composer from components
export { Composer } from "./components";
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
@use "./header.scss";
@use "./threadList.scss";
@use "./thread.scss";
@use "./components/composer.scss";
@use "./conversationStarter.scss";
@use "./welcomeScreen.scss";
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useThread } from "@openuidev/react-headless";
import clsx from "clsx";
import { ArrowUp, Square } from "lucide-react";
import { useLayoutEffect, useRef } from "react";
import { useComposerState } from "../../../hooks/useComposerState";
import { IconButton } from "../../IconButton";

export interface ComposerProps {
className?: string;
placeholder?: string;
}

export const Composer = ({ className, placeholder = "Type your message..." }: ComposerProps) => {
const { textContent, setTextContent } = useComposerState();
const processMessage = useThread((s) => s.processMessage);
const cancelMessage = useThread((s) => s.cancelMessage);
const isRunning = useThread((s) => s.isRunning);
const isLoadingMessages = useThread((s) => s.isLoadingMessages);
const inputRef = useRef<HTMLTextAreaElement>(null);

const handleSubmit = () => {
if (!textContent.trim() || isRunning || isLoadingMessages) {
return;
}

processMessage({
role: "user",
content: textContent,
});

setTextContent("");
};

useLayoutEffect(() => {
const input = inputRef.current;
if (!input) return;

// Reset to 0 (not "auto") so scrollHeight reflects content, not container
input.style.height = "0px";
input.style.height = `${Math.max(input.scrollHeight, 24)}px`;
}, [textContent]);

return (
<div
className={clsx("openui-bottom-tray-thread-composer", className)}
onClick={(e) => {
if (!(e.target as HTMLElement).closest("button, a, [role='button']")) {
inputRef.current?.focus();
}
}}
>
<div className="openui-bottom-tray-thread-composer__input-wrapper">
<textarea
ref={inputRef}
value={textContent}
onChange={(e) => setTextContent(e.target.value)}
className="openui-bottom-tray-thread-composer__input"
placeholder={placeholder}
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}}
/>
<div className="openui-bottom-tray-thread-composer__action-bar">
<IconButton
onClick={isRunning ? cancelMessage : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowUp size="1em" />}
size="medium"
variant="primary"
className="openui-bottom-tray-thread-composer__submit-button"
/>
</div>
</div>
</div>
);
};

export default Composer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@use "../../../cssUtils" as cssUtils;

.openui-bottom-tray-thread-composer {
width: 100%;
padding: 0 cssUtils.$space-s cssUtils.$space-s;

&__input-wrapper {
background-color: cssUtils.$foreground;
border: 1px solid cssUtils.$border-interactive;
border-radius: cssUtils.$radius-l;
box-shadow: cssUtils.$shadow-s;
overflow: clip;
display: flex;
flex-direction: column;
gap: cssUtils.$space-s;
padding: cssUtils.$space-s;
}

&__input {
scrollbar-width: thin;
scrollbar-color: cssUtils.$elevated transparent;

&::-webkit-scrollbar {
width: 6px;
}

&::-webkit-scrollbar-track {
background: transparent;
}

&::-webkit-scrollbar-thumb {
background-color: cssUtils.$elevated;
border-radius: cssUtils.$radius-full;
}

padding: 0 cssUtils.$space-3xs;
resize: none;
max-height: 200px;
outline: none;
border: none;
background: transparent;
overflow-y: auto;
@include cssUtils.typography(body, default);
color: cssUtils.$text-neutral-primary;

&::placeholder {
color: cssUtils.$text-neutral-secondary;
}
}

&__action-bar {
display: flex;
align-items: center;
}

&__submit-button {
margin-left: auto;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Composer } from "./Composer";
export type { ComposerProps } from "./Composer";
34 changes: 0 additions & 34 deletions packages/react-ui/src/components/BottomTray/thread.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,37 +101,3 @@
height: fit-content;
}
}

.openui-bottom-tray-thread-composer {
width: 100%;
padding: 0 cssUtils.$space-s cssUtils.$space-s;

&__input-wrapper {
background-color: cssUtils.$foreground;
border: 1.256px solid cssUtils.$border-default;
display: flex;
align-items: flex-end;
gap: cssUtils.$space-s;
padding: cssUtils.$space-m;
border-radius: cssUtils.$radius-xl;
}

&__input {
flex-grow: 1;
padding: 0;
resize: none;
margin: auto 0px;
min-height: 1lh;
max-height: 154px;
outline: none;
border: none;
background: transparent;
overflow: hidden;

@include cssUtils.typography(primary, default);
color: cssUtils.$text-neutral-primary;
&::placeholder {
color: cssUtils.$text-neutral-tertiary;
}
}
}
63 changes: 3 additions & 60 deletions packages/react-ui/src/components/CopilotShell/Thread.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { AssistantMessage, Message, ToolMessage } from "@openuidev/react-headless";
import { MessageProvider, useThread } from "@openuidev/react-headless";
import clsx from "clsx";
import { ArrowUp, Square } from "lucide-react";
import React, { memo, useEffect, useLayoutEffect, useRef } from "react";
import { useComposerState } from "../../hooks/useComposerState";
import React, { memo, useEffect, useRef } from "react";
import { ScrollVariant, useScrollToBottom } from "../../hooks/useScrollToBottom";
import { IconButton } from "../IconButton";
import { MarkDownRenderer } from "../MarkDownRenderer";
import { MessageLoading as MessageLoadingComponent } from "../MessageLoading";
import type { AssistantMessageComponent, UserMessageComponent } from "../OpenUIChat/types";
Expand Down Expand Up @@ -292,59 +289,5 @@ export const Messages = ({
);
};

export const Composer = ({ className }: { className?: string }) => {
const { textContent, setTextContent } = useComposerState();
const processMessage = useThread((s) => s.processMessage);
const cancelMessage = useThread((s) => s.cancelMessage);
const isRunning = useThread((s) => s.isRunning);
const inputRef = useRef<HTMLTextAreaElement>(null);

const handleSubmit = () => {
if (!textContent.trim() || isRunning) {
return;
}

processMessage({
role: "user",
content: textContent,
});

setTextContent("");
};

useLayoutEffect(() => {
const input = inputRef.current;
if (!input) {
return;
}

input.style.height = "auto";
input.style.height = `${input.scrollHeight}px`;
}, [textContent]);

return (
<div className={clsx("openui-copilot-shell-thread-composer", className)}>
<div className="openui-copilot-shell-thread-composer__input-wrapper">
<textarea
ref={inputRef}
autoFocus
rows={1}
value={textContent}
onChange={(e) => setTextContent(e.target.value)}
className="openui-copilot-shell-thread-composer__input"
placeholder="Type your message..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}}
/>
<IconButton
onClick={isRunning ? cancelMessage : handleSubmit}
icon={isRunning ? <Square size="1em" fill="currentColor" /> : <ArrowUp size="1em" />}
/>
</div>
</div>
);
};
// Re-export Composer from components
export { Composer } from "./components";
Loading
Loading