From 6d0c004269c3e97223838b702a7a8a302ae2c618 Mon Sep 17 00:00:00 2001 From: Nicholas Kissel Date: Tue, 10 Mar 2026 23:10:42 -0700 Subject: [PATCH] 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 --- docs/openapi.json | 2 +- .../frontend/src/components/mock-layout.tsx | 32 +++--- .../mock-layout/history-minimap.tsx | 2 +- .../components/mock-layout/message-list.tsx | 19 ++-- .../components/mock-layout/model-picker.tsx | 13 ++- .../mock-layout/prompt-composer.tsx | 27 +++-- .../src/components/mock-layout/sidebar.tsx | 98 ++++++++++--------- .../src/components/workspace-dashboard.tsx | 47 +++++---- frontend/packages/inspector/index.html | 7 +- sdks/react/src/ChatComposer.tsx | 3 + 10 files changed, 142 insertions(+), 108 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 262f639..a984b28 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "0.3.0" + "version": "0.3.1" }, "servers": [ { diff --git a/factory/packages/frontend/src/components/mock-layout.tsx b/factory/packages/frontend/src/components/mock-layout.tsx index 46402a7..5fdff9d 100644 --- a/factory/packages/frontend/src/components/mock-layout.tsx +++ b/factory/packages/frontend/src/components/mock-layout.tsx @@ -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({ }} >

Create the first session

-

Sessions are where you chat with the agent. Start one now to send the first prompt on this handoff.

+

+ Sessions are where you chat with the agent. Start one now to send the first prompt on this task. +

diff --git a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx index 1021224..a5a91cf 100644 --- a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx +++ b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx @@ -43,7 +43,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: >
- Handoff Events + Task Events {events.length}
diff --git a/factory/packages/frontend/src/components/mock-layout/message-list.tsx b/factory/packages/frontend/src/components/mock-layout/message-list.tsx index e213330..5e2ccbb 100644 --- a/factory/packages/frontend/src/components/mock-layout/message-list.tsx +++ b/factory/packages/frontend/src/components/mock-layout/message-list.tsx @@ -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 ( <> + {historyEvents.length > 0 ? : null}
void; }) { const [css, theme] = useStyletron(); + const [isOpen, setIsOpen] = useState(false); return ( 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)} - + {isOpen ? : }
diff --git a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx index e809180..d036d03 100644 --- a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx +++ b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx @@ -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({
(isRunning ? : )} + renderFooter={() => ( +
+ +
+ )} /> -
); }); diff --git a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx index 9a02f96..51dd241 100644 --- a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx @@ -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>({}); + const [collapsedProjects, setCollapsedProjects] = useState>({}); return ( + - - Handoffs + + + Tasks - ) : null} ); })} diff --git a/factory/packages/frontend/src/components/workspace-dashboard.tsx b/factory/packages/frontend/src/components/workspace-dashboard.tsx index 97002c8..7abe8a5 100644 --- a/factory/packages/frontend/src/components/workspace-dashboard.tsx +++ b/factory/packages/frontend/src/components/workspace-dashboard.tsx @@ -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}`, })} > - Handoffs + Tasks @@ -845,7 +845,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) : null} - {!handoffsQuery.isLoading && repoGroups.length === 0 ? No repos or handoffs yet. Add a repo to start a workspace. : null} + {!handoffsQuery.isLoading && repoGroups.length === 0 ? ( + No repos or tasks yet. Add a repo to start a workspace. + ) : null} {repoGroups.map((group) => (
- Create Handoff + Create Task
@@ -1172,7 +1174,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep {formatRelativeAge(branch.updatedAt)} - {branch.handoffId ? "handoff" : "unmapped"} + + {branch.handoffId ? "task" : "unmapped"} + {branch.trackedInStack ? stack : null} @@ -1264,7 +1268,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid={`repo-overview-create-${branchToken}`} > - Create Handoff + Create Task ) : null} @@ -1302,7 +1306,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep > - {selectedForSession ? (selectedForSession.title ?? "Determining title...") : "No handoff selected"} + {selectedForSession ? selectedForSession.title ?? "Determining title..." : "No task selected"} {selectedForSession ? {selectedForSession.status} : null} @@ -1333,7 +1337,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep })} > {!selectedForSession ? ( - Select a handoff from the left sidebar. + Select a task from the left sidebar. ) : ( <>
) : null} @@ -1525,7 +1529,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep - {repoOverviewMode ? "Repo Details" : "Handoff Details"} + {repoOverviewMode ? "Repo Details" : "Task Details"} @@ -1577,7 +1581,10 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep - +
)} @@ -1585,7 +1592,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) ) : !selectedForSession ? ( - No handoff selected. + No task selected. ) : ( <> @@ -1601,7 +1608,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep gap: theme.sizing.scale300, })} > - + @@ -1728,7 +1735,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} overrides={modalOverrides} > - Create Handoff + Create Task
- 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.
@@ -1876,7 +1883,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid="handoff-create-submit" > - Create Handoff + Create Task diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index e28187b..5893717 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -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 { diff --git a/sdks/react/src/ChatComposer.tsx b/sdks/react/src/ChatComposer.tsx index 38b8520..7f92ebd 100644 --- a/sdks/react/src/ChatComposer.tsx +++ b/sdks/react/src/ChatComposer.tsx @@ -26,6 +26,7 @@ export interface ChatComposerProps { rows?: number; textareaProps?: Omit, "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?.()}