mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
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:
parent
34a0587cbc
commit
6d0c004269
10 changed files with 142 additions and 108 deletions
|
|
@ -10,7 +10,7 @@
|
|||
"license": {
|
||||
"name": "Apache-2.0"
|
||||
},
|
||||
"version": "0.3.0"
|
||||
"version": "0.3.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue