feat: modernize chat UI and rename handoff to task

- Remove agent message bubbles, keep user bubbles (right-aligned)
- Rename "Handoffs" to "Tasks" with ListChecks icon in sidebar
- Move model picker inside composer, add renderFooter to ChatComposer SDK
- Make project sections collapsible with hover-only chevrons
- Remove divider between chat and composer
- Update model picker chevron to flip on open/close
- Replace all user-visible "handoff" strings with "task" across frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicholas Kissel 2026-03-10 23:10:42 -07:00
parent 34a0587cbc
commit 6d0c004269
10 changed files with 142 additions and 108 deletions

View file

@ -10,7 +10,7 @@
"license": {
"name": "Apache-2.0"
},
"version": "0.3.0"
"version": "0.3.1"
},
"servers": [
{

View file

@ -352,7 +352,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
const changeModel = useCallback(
(model: ModelId) => {
if (!promptTab) {
throw new Error(`Unable to change model for handoff ${handoff.id} without an active prompt tab`);
throw new Error(`Unable to change model for task ${handoff.id} without an active prompt tab`);
}
void handoffWorkbenchClient.changeModel({
@ -487,7 +487,9 @@ const TranscriptPanel = memo(function TranscriptPanel({
}}
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create the first session</h2>
<p style={{ margin: 0, opacity: 0.75 }}>Sessions are where you chat with the agent. Start one now to send the first prompt on this handoff.</p>
<p style={{ margin: 0, opacity: 0.75 }}>
Sessions are where you chat with the agent. Start one now to send the first prompt on this task.
</p>
<button
type="button"
onClick={addTab}
@ -661,15 +663,15 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
void (async () => {
const repoId = activeHandoff?.repoId ?? viewModel.repos[0]?.id ?? "";
if (!repoId) {
throw new Error("Cannot create a handoff without an available repo");
throw new Error("Cannot create a task without an available repo");
}
const task = window.prompt("Describe the handoff task", "Investigate and implement the requested change");
const task = window.prompt("Describe the task", "Investigate and implement the requested change");
if (!task) {
return;
}
const title = window.prompt("Optional handoff title", "")?.trim() || undefined;
const title = window.prompt("Optional task title", "")?.trim() || undefined;
const branch = window.prompt("Optional branch name", "")?.trim() || undefined;
const { handoffId, tabId } = await handoffWorkbenchClient.createHandoff({
repoId,
@ -692,7 +694,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const openDiffTab = useCallback(
(path: string) => {
if (!activeHandoff) {
throw new Error("Cannot open a diff tab without an active handoff");
throw new Error("Cannot open a diff tab without an active task");
}
setOpenDiffsByHandoff((current) => {
const existing = sanitizeOpenDiffs(activeHandoff, current[activeHandoff.id]);
@ -736,10 +738,10 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
(id: string) => {
const currentHandoff = handoffs.find((handoff) => handoff.id === id);
if (!currentHandoff) {
throw new Error(`Unable to rename missing handoff ${id}`);
throw new Error(`Unable to rename missing task ${id}`);
}
const nextTitle = window.prompt("Rename handoff", currentHandoff.title);
const nextTitle = window.prompt("Rename task", currentHandoff.title);
if (nextTitle === null) {
return;
}
@ -758,7 +760,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
(id: string) => {
const currentHandoff = handoffs.find((handoff) => handoff.id === id);
if (!currentHandoff) {
throw new Error(`Unable to rename missing handoff ${id}`);
throw new Error(`Unable to rename missing task ${id}`);
}
const nextBranch = window.prompt("Rename branch", currentHandoff.branch ?? "");
@ -778,14 +780,14 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const archiveHandoff = useCallback(() => {
if (!activeHandoff) {
throw new Error("Cannot archive without an active handoff");
throw new Error("Cannot archive without an active task");
}
void handoffWorkbenchClient.archiveHandoff({ handoffId: activeHandoff.id });
}, [activeHandoff]);
const publishPr = useCallback(() => {
if (!activeHandoff) {
throw new Error("Cannot publish PR without an active handoff");
throw new Error("Cannot publish PR without an active task");
}
void handoffWorkbenchClient.publishPr({ handoffId: activeHandoff.id });
}, [activeHandoff]);
@ -793,7 +795,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
const revertFile = useCallback(
(path: string) => {
if (!activeHandoff) {
throw new Error("Cannot revert a file without an active handoff");
throw new Error("Cannot revert a file without an active task");
}
setOpenDiffsByHandoff((current) => ({
...current,
@ -968,10 +970,10 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
gap: "12px",
}}
>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first handoff</h2>
<h2 style={{ margin: 0, fontSize: "20px", fontWeight: 600 }}>Create your first task</h2>
<p style={{ margin: 0, opacity: 0.75 }}>
{viewModel.repos.length > 0
? "Start from the sidebar to create a handoff on the first available repo."
? "Start from the sidebar to create a task on the first available repo."
: "No repos are available in this workspace yet."}
</p>
<button
@ -989,7 +991,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
fontWeight: 600,
}}
>
New handoff
New task
</button>
</div>
</div>

View file

@ -43,7 +43,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }:
>
<div className={css({ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "6px" })}>
<LabelXSmall color={theme.colors.contentTertiary} $style={{ letterSpacing: "0.08em", textTransform: "uppercase" }}>
Handoff Events
Task Events
</LabelXSmall>
<LabelXSmall color={theme.colors.contentTertiary}>{events.length}</LabelXSmall>
</div>

View file

@ -55,11 +55,11 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
borderBottomRightRadius: "4px",
}
: {
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: `1px solid ${theme.colors.borderOpaque}`,
backgroundColor: "transparent",
border: "none",
color: "#e4e4e7",
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "16px",
borderRadius: "0",
padding: "0",
}),
})}
>
@ -163,12 +163,6 @@ export const MessageList = memo(function MessageList({
}),
message: css({
display: "flex",
'&[data-variant="user"]': {
justifyContent: "flex-end",
},
'&[data-variant="assistant"]': {
justifyContent: "flex-start",
},
}),
messageContent: messageContentClass,
messageText: css({
@ -193,6 +187,11 @@ export const MessageList = memo(function MessageList({
return (
<>
<style>{`
[data-variant="user"] > [data-slot="message-content"] {
margin-left: auto;
}
`}</style>
{historyEvents.length > 0 ? <HistoryMinimap events={historyEvents} onSelect={onSelectHistoryEvent} /> : null}
<div
ref={scrollRef}

View file

@ -1,7 +1,7 @@
import { memo, useState } from "react";
import { useStyletron } from "baseui";
import { StatefulPopover, PLACEMENT } from "baseui/popover";
import { ChevronDown, Star } from "lucide-react";
import { ChevronDown, ChevronUp, Star } from "lucide-react";
import { AgentIcon } from "./ui";
import { MODEL_GROUPS, modelLabel, providerAgent, type ModelId } from "./view-model";
@ -100,12 +100,15 @@ export const ModelPicker = memo(function ModelPicker({
onSetDefault: (id: ModelId) => void;
}) {
const [css, theme] = useStyletron();
const [isOpen, setIsOpen] = useState(false);
return (
<StatefulPopover
placement={PLACEMENT.topLeft}
triggerType="click"
autoFocus={false}
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
overrides={{
Body: {
style: {
@ -141,13 +144,13 @@ export const ModelPicker = memo(function ModelPicker({
fontSize: "12px",
fontWeight: 500,
color: theme.colors.contentSecondary,
backgroundColor: theme.colors.backgroundTertiary,
border: `1px solid ${theme.colors.borderOpaque}`,
":hover": { color: theme.colors.contentPrimary },
backgroundColor: "rgba(255, 255, 255, 0.10)",
border: "1px solid rgba(255, 255, 255, 0.14)",
":hover": { color: theme.colors.contentPrimary, backgroundColor: "rgba(255, 255, 255, 0.14)" },
})}
>
{modelLabel(value)}
<ChevronDown size={11} />
{isOpen ? <ChevronDown size={11} /> : <ChevronUp size={11} />}
</button>
</div>
</StatefulPopover>

View file

@ -43,25 +43,27 @@ export const PromptComposer = memo(function PromptComposer({
backgroundColor: "rgba(255, 255, 255, 0.06)",
border: `1px solid ${theme.colors.borderOpaque}`,
borderRadius: "16px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`,
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
transition: "border-color 200ms ease",
":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" },
display: "flex",
flexDirection: "column",
}),
input: css({
display: "block",
width: "100%",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`,
padding: "12px 58px 12px 14px",
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 20}px`,
padding: "14px 58px 8px 14px",
background: "transparent",
border: "none",
borderRadius: "16px",
borderRadius: "16px 16px 0 0",
color: theme.colors.contentPrimary,
fontSize: "13px",
fontFamily: "inherit",
resize: "none",
outline: "none",
lineHeight: "1.4",
maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT}px`,
maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT + 40}px`,
boxSizing: "border-box",
overflowY: "hidden",
"::placeholder": { color: theme.colors.contentSecondary },
@ -101,7 +103,7 @@ export const PromptComposer = memo(function PromptComposer({
<div
className={css({
padding: "12px 16px",
borderTop: `1px solid ${theme.colors.borderOpaque}`,
borderTop: "none",
flexShrink: 0,
display: "flex",
flexDirection: "column",
@ -151,13 +153,22 @@ export const PromptComposer = memo(function PromptComposer({
}}
placeholder={placeholder}
inputRef={textareaRef}
rows={1}
rows={2}
allowEmptySubmit={isRunning}
submitLabel={isRunning ? "Stop" : "Send"}
classNames={composerClassNames}
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <ArrowUpFromLine size={16} />)}
renderFooter={() => (
<div className={css({ padding: "0 10px 8px" })}>
<ModelPicker
value={model}
defaultModel={defaultModel}
onChange={onChangeModel}
onSetDefault={onSetDefaultModel}
/>
</div>
)}
/>
<ModelPicker value={model} defaultModel={defaultModel} onChange={onChangeModel} onSetDefault={onSetDefaultModel} />
</div>
);
});

View file

@ -1,7 +1,7 @@
import { memo, useState } from "react";
import { useStyletron } from "baseui";
import { LabelSmall, LabelXSmall } from "baseui/typography";
import { CloudUpload, GitPullRequestDraft, Plus } from "lucide-react";
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
import { formatRelativeAge, type Handoff, type ProjectSection } from "./view-model";
import { ContextMenuOverlay, HandoffIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
@ -25,13 +25,22 @@ export const Sidebar = memo(function Sidebar({
}) {
const [css, theme] = useStyletron();
const contextMenu = useContextMenu();
const [expandedProjects, setExpandedProjects] = useState<Record<string, boolean>>({});
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
return (
<SPanel>
<style>{`
[data-project-header]:hover [data-chevron] {
display: inline-flex !important;
}
`}</style>
<PanelHeaderBar>
<LabelSmall color={theme.colors.contentPrimary} $style={{ fontWeight: 600, flex: 1, fontSize: "13px" }}>
Handoffs
<LabelSmall
color={theme.colors.contentPrimary}
$style={{ fontWeight: 600, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
>
<ListChecks size={14} />
Tasks
</LabelSmall>
<button
onClick={onCreate}
@ -56,38 +65,58 @@ export const Sidebar = memo(function Sidebar({
<ScrollBody>
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
{projects.map((project) => {
const visibleCount = expandedProjects[project.id] ? project.handoffs.length : Math.min(project.handoffs.length, 5);
const hiddenCount = Math.max(0, project.handoffs.length - visibleCount);
const isCollapsed = collapsedProjects[project.id] === true;
return (
<div key={project.id} className={css({ display: "flex", flexDirection: "column", gap: "4px" })}>
<div
onClick={() =>
setCollapsedProjects((current) => ({
...current,
[project.id]: !current[project.id],
}))
}
data-project-header
className={css({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "10px 8px 4px",
gap: "8px",
cursor: "pointer",
userSelect: "none",
":hover": { opacity: 0.8 },
})}
>
<LabelSmall
color={theme.colors.contentSecondary}
$style={{
fontSize: "11px",
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{project.label}
</LabelSmall>
<LabelXSmall color={theme.colors.contentTertiary}>{formatRelativeAge(project.updatedAtMs)}</LabelXSmall>
<div className={css({ display: "flex", alignItems: "center", gap: "4px", overflow: "hidden" })}>
<span className={css({ display: "none", flexShrink: 0 })} data-chevron>
{isCollapsed ? (
<ChevronDown size={12} color={theme.colors.contentTertiary} />
) : (
<ChevronUp size={12} color={theme.colors.contentTertiary} />
)}
</span>
<LabelSmall
color={theme.colors.contentSecondary}
$style={{
fontSize: "11px",
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{project.label}
</LabelSmall>
</div>
<LabelXSmall color={theme.colors.contentTertiary}>
{formatRelativeAge(project.updatedAtMs)}
</LabelXSmall>
</div>
{project.handoffs.slice(0, visibleCount).map((handoff) => {
{!isCollapsed && project.handoffs.map((handoff) => {
const isActive = handoff.id === activeId;
const isDim = handoff.status === "archived";
const isRunning = handoff.tabs.some((tab) => tab.status === "running");
@ -103,7 +132,7 @@ export const Sidebar = memo(function Sidebar({
onClick={() => onSelect(handoff.id)}
onContextMenu={(event) =>
contextMenu.open(event, [
{ label: "Rename handoff", onClick: () => onRenameHandoff(handoff.id) },
{ label: "Rename task", onClick: () => onRenameHandoff(handoff.id) },
{ label: "Rename branch", onClick: () => onRenameBranch(handoff.id) },
{ label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) },
])
@ -111,7 +140,7 @@ export const Sidebar = memo(function Sidebar({
className={css({
padding: "12px",
borderRadius: "8px",
border: isActive ? "1px solid rgba(255, 255, 255, 0.2)" : "1px solid transparent",
border: "1px solid transparent",
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
cursor: "pointer",
transition: "all 200ms ease",
@ -184,27 +213,6 @@ export const Sidebar = memo(function Sidebar({
);
})}
{hiddenCount > 0 ? (
<button
type="button"
onClick={() =>
setExpandedProjects((current) => ({
...current,
[project.id]: true,
}))
}
className={css({
all: "unset",
padding: "8px 12px 10px 34px",
color: theme.colors.contentSecondary,
fontSize: "12px",
cursor: "pointer",
":hover": { color: theme.colors.contentPrimary },
})}
>
Show {hiddenCount} more
</button>
) : null}
</div>
);
})}

View file

@ -86,7 +86,7 @@ const DetailRail = styled("aside", ({ $theme }) => ({
const FILTER_OPTIONS: SelectItem[] = [
{ id: "active", label: "Active + Unmapped" },
{ id: "archived", label: "Archived Handoffs" },
{ id: "archived", label: "Archived Tasks" },
{ id: "unmapped", label: "Unmapped Only" },
{ id: "all", label: "All Branches" },
];
@ -394,7 +394,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
refetchInterval: 2_500,
queryFn: async () => {
if (!selectedHandoffId) {
throw new Error("No handoff");
throw new Error("No task selected");
}
return backendClient.getHandoff(workspaceId, selectedHandoffId);
},
@ -532,7 +532,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
const startSessionFromHandoff = async (): Promise<{ id: string; status: "running" | "idle" | "error" }> => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this handoff");
throw new Error("No sandbox is available for this task");
}
return backendClient.createSandboxSession({
workspaceId,
@ -565,7 +565,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
const sendPrompt = useMutation({
mutationFn: async (prompt: string) => {
if (!selectedForSession || !activeSandbox?.sandboxId) {
throw new Error("No sandbox is available for this handoff");
throw new Error("No sandbox is available for this task");
}
const sessionId = await ensureSessionForPrompt();
await backendClient.sendSandboxPrompt({
@ -834,7 +834,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
borderTop: `1px solid ${theme.colors.borderOpaque}`,
})}
>
<LabelXSmall color="contentSecondary">Handoffs</LabelXSmall>
<LabelXSmall color="contentSecondary">Tasks</LabelXSmall>
</div>
</PanelHeader>
@ -845,7 +845,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
</>
) : null}
{!handoffsQuery.isLoading && repoGroups.length === 0 ? <EmptyState>No repos or handoffs yet. Add a repo to start a workspace.</EmptyState> : null}
{!handoffsQuery.isLoading && repoGroups.length === 0 ? (
<EmptyState>No repos or tasks yet. Add a repo to start a workspace.</EmptyState>
) : null}
{repoGroups.map((group) => (
<section
@ -960,7 +962,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid={group.repoId === createRepoId ? "handoff-create-open" : `handoff-create-open-${group.repoId}`}
>
Create Handoff
Create Task
</Button>
</div>
</section>
@ -1172,7 +1174,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
{formatRelativeAge(branch.updatedAt)}
</ParagraphSmall>
<StatusPill kind={branch.handoffId ? "positive" : "warning"}>{branch.handoffId ? "handoff" : "unmapped"}</StatusPill>
<StatusPill kind={branch.handoffId ? "positive" : "warning"}>
{branch.handoffId ? "task" : "unmapped"}
</StatusPill>
{branch.trackedInStack ? <StatusPill kind="neutral">stack</StatusPill> : null}
</div>
</div>
@ -1264,7 +1268,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid={`repo-overview-create-${branchToken}`}
>
Create Handoff
Create Task
</Button>
) : null}
@ -1302,7 +1306,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
>
<Bot size={16} />
<HeadingXSmall marginTop="0" marginBottom="0">
{selectedForSession ? (selectedForSession.title ?? "Determining title...") : "No handoff selected"}
{selectedForSession ? selectedForSession.title ?? "Determining title..." : "No task selected"}
</HeadingXSmall>
{selectedForSession ? <StatusPill kind={statusKind(selectedForSession.status)}>{selectedForSession.status}</StatusPill> : null}
</div>
@ -1333,7 +1337,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
})}
>
{!selectedForSession ? (
<EmptyState>Select a handoff from the left sidebar.</EmptyState>
<EmptyState>Select a task from the left sidebar.</EmptyState>
) : (
<>
<div
@ -1409,12 +1413,12 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
: !activeSandbox?.sandboxId
? selectedForSession.statusMessage
? `Sandbox unavailable: ${selectedForSession.statusMessage}`
: "This handoff is still provisioning its sandbox."
: "This task is still provisioning its sandbox."
: staleSessionId
? `Session ${staleSessionId} is unavailable. Start a new session to continue.`
: resolvedSessionId
? "No transcript events yet. Send a prompt to start this session."
: "No active session for this handoff."}
: "No active session for this task."}
</EmptyState>
) : null}
@ -1525,7 +1529,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
<DetailRail>
<PanelHeader>
<HeadingSmall marginTop="0" marginBottom="0">
{repoOverviewMode ? "Repo Details" : "Handoff Details"}
{repoOverviewMode ? "Repo Details" : "Task Details"}
</HeadingSmall>
</PanelHeader>
@ -1577,7 +1581,10 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
<MetaRow label="Parent" value={selectedBranchOverview.parentBranch ?? "-"} mono />
<MetaRow label="Commit" value={selectedBranchOverview.commitSha.slice(0, 10)} mono />
<MetaRow label="Diff" value={formatDiffStat(selectedBranchOverview.diffStat)} />
<MetaRow label="Handoff" value={selectedBranchOverview.handoffTitle ?? selectedBranchOverview.handoffId ?? "-"} />
<MetaRow
label="Task"
value={selectedBranchOverview.handoffTitle ?? selectedBranchOverview.handoffId ?? "-"}
/>
</div>
)}
</section>
@ -1585,7 +1592,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
)
) : !selectedForSession ? (
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
No handoff selected.
No task selected.
</ParagraphSmall>
) : (
<>
@ -1601,7 +1608,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
gap: theme.sizing.scale300,
})}
>
<MetaRow label="Handoff" value={selectedForSession.handoffId} mono />
<MetaRow label="Task" value={selectedForSession.handoffId} mono />
<MetaRow label="Sandbox" value={selectedForSession.activeSandboxId ?? "-"} mono />
<MetaRow label="Session" value={resolvedSessionId ?? "-"} mono />
</div>
@ -1728,7 +1735,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
overrides={modalOverrides}
>
<ModalHeader>Create Handoff</ModalHeader>
<ModalHeader>Create Task</ModalHeader>
<ModalBody>
<div
className={css({
@ -1738,7 +1745,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
})}
>
<ParagraphSmall marginTop="0" marginBottom="0" color="contentSecondary">
Pick a repo, describe the task, and the backend will create a handoff.
Pick a repo, describe the task, and the backend will create a task.
</ParagraphSmall>
<div>
@ -1876,7 +1883,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
}}
data-testid="handoff-create-submit"
>
Create Handoff
Create Task
</Button>
</ModalFooter>
</Modal>

View file

@ -1510,10 +1510,11 @@
}
.message.assistant .message-content {
background: var(--surface);
border: 1px solid var(--border);
background: none;
border: none;
color: var(--text-secondary);
border-bottom-left-radius: 4px;
border-radius: 0;
padding: 0 16px;
}
.message.system .avatar {

View file

@ -26,6 +26,7 @@ export interface ChatComposerProps {
rows?: number;
textareaProps?: Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "className" | "disabled" | "onChange" | "onKeyDown" | "placeholder" | "rows" | "value">;
renderSubmitContent?: () => ReactNode;
renderFooter?: () => ReactNode;
}
const DEFAULT_CLASS_NAMES: ChatComposerClassNames = {
@ -62,6 +63,7 @@ export const ChatComposer = ({
rows = 1,
textareaProps,
renderSubmitContent,
renderFooter,
}: ChatComposerProps) => {
const resolvedClassNames = mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides);
const isSubmitDisabled = disabled || submitDisabled || (!allowEmptySubmit && message.trim().length === 0);
@ -92,6 +94,7 @@ export const ChatComposer = ({
rows={rows}
disabled={disabled}
/>
{renderFooter?.()}
<button
type="submit"
className={resolvedClassNames.submit}