diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/ActiveSubtasksList.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/ActiveSubtasksList.tsx index e289e408..f52a9b92 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/ActiveSubtasksList.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/ActiveSubtasksList.tsx @@ -119,7 +119,7 @@ function ActiveSubtasksListContent({ taskEntryKey }: ActiveSubtasksListProps) { return (
- + void; + embedded?: boolean; } export function DraftPromptBanner({ draftPrompt, onClick, + embedded = false, }: DraftPromptBannerProps) { const interactive = !!onClick; - return ( -
-
-
{ - if (e.key === 'Enter' || e.key === ' ') { - onClick(); - } + const banner = ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + onClick(); } - : undefined - } - > -
- {draftPrompt} -
- {interactive && ( - - )} - + } + : undefined + } + > +
+ {draftPrompt}
+ {interactive && ( + + )} +
); + + return embedded ? banner :
{banner}
; } diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/HistoricalContent.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/HistoricalContent.tsx index 9726cfd6..073c1fcd 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/HistoricalContent.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/HistoricalContent.tsx @@ -79,7 +79,7 @@ export function HistoricalContent({ session, footer }: HistoricalContentProps) {
- + {shouldShowWakeTaskInput && taskRun ? ( - - ) : ( - isResuming && - draftPrompt && ( - - ) - )} + + + + ) : isResuming && draftPrompt ? ( + + + + ) : null}
@@ -115,6 +117,14 @@ export function HistoricalContent({ session, footer }: HistoricalContentProps) { ); } +function HistoricalInputTray({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + function ClosePreviewOnSleepEffect({ asleep }: { asleep: boolean }) { useClosePreviewOnSleep(asleep); return null; diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/LiveContent.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/LiveContent.tsx index 6c3b85e9..b77e6faa 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/LiveContent.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/LiveContent.tsx @@ -259,20 +259,16 @@ function LiveContentInner({ } />
-
- - - -
+ + +
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Messages.client.test.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Messages.client.test.tsx index 33a66b54..7e49c9d8 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/Messages.client.test.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/Messages.client.test.tsx @@ -88,6 +88,20 @@ vi.mock('./messages/index', () => ({ vi.mock('./messages/acp', () => ({ AcpMessageItem: ({ msg }: { msg: { id: string } }) =>
{msg.id}
, AcpGroupedToolMessage: () => null, + AcpActivityGroupMessage: ({ + group, + children, + }: { + group: { ts: number; endTs: number }; + children: ReactNode; + }) => ( +
+ +
{children}
+
+ ), AcpTextMessage: ({ msg }: { msg: { text?: string } }) => (
{msg.text}
), @@ -482,4 +496,208 @@ describe('Messages', () => { }), ); }); + + it('collapses eligible background activity between text messages', () => { + mockBuildAcpRenderBlocks.mockReturnValue([ + { + kind: 'message', + msg: { + id: 'assistant-text-1', + ts: 1_000, + role: 'assistant', + kind: 'text', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'reasoning-1', + ts: 2_000, + role: 'assistant', + kind: 'reasoning', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'assistant-text-2', + ts: 19_000, + role: 'assistant', + kind: 'text', + partial: false, + }, + }, + ] as never); + + render( + , + ); + + expect(screen.getByText('Worked for 17s')).toBeInTheDocument(); + expect(screen.getByText('reasoning-1')).toBeInTheDocument(); + }); + + it('uses the rendered session prompt as the left boundary for initial activity', () => { + mockBuildAcpRenderBlocks.mockReturnValue([ + { + kind: 'message', + msg: { + id: 'reasoning-1', + ts: 2_000, + role: 'assistant', + kind: 'reasoning', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'assistant-text-1', + ts: 9_000, + role: 'assistant', + kind: 'text', + partial: false, + }, + }, + ] as never); + + render( + , + ); + + expect(screen.getByText('Initial prompt')).toBeInTheDocument(); + expect(screen.getByText('Worked for 7s')).toBeInTheDocument(); + expect(screen.getByText('reasoning-1')).toBeInTheDocument(); + }); + + it('collapses initial eligible activity even when there is no visible starting text message', () => { + mockBuildAcpRenderBlocks.mockReturnValue([ + { + kind: 'message', + msg: { + id: 'reasoning-1', + ts: 2_000, + role: 'assistant', + kind: 'reasoning', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'assistant-text-1', + ts: 12_000, + role: 'assistant', + kind: 'text', + partial: false, + }, + }, + ] as never); + + render( + , + ); + + expect(screen.getByText('Worked for 10s')).toBeInTheDocument(); + expect(screen.getByText('reasoning-1')).toBeInTheDocument(); + }); + + it('keeps todo section markers visible while collapsing following activity', () => { + mockBuildAcpRenderBlocks.mockReturnValue([ + { + kind: 'message', + msg: { + id: 'reasoning-1', + ts: 2_000, + role: 'assistant', + kind: 'reasoning', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'todo-1', + ts: 3_000, + role: 'assistant', + kind: 'todo_section', + partial: false, + data: { + todoId: 'todo-1', + content: 'Inspect repository guidance', + }, + }, + }, + { + kind: 'message', + msg: { + id: 'reasoning-2', + ts: 4_000, + role: 'assistant', + kind: 'reasoning', + partial: false, + }, + }, + { + kind: 'message', + msg: { + id: 'assistant-text-1', + ts: 12_000, + role: 'assistant', + kind: 'text', + partial: false, + }, + }, + ] as never); + + render( + , + ); + + expect(screen.getByText('Worked for 8s')).toBeInTheDocument(); + expect(screen.getByText('reasoning-1')).toBeInTheDocument(); + expect(screen.getByText('todo-1')).toBeInTheDocument(); + expect(screen.getByText('reasoning-2')).toBeInTheDocument(); + }); }); diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Messages.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Messages.tsx index 70ae33ec..2c6e81c8 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/Messages.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/Messages.tsx @@ -40,8 +40,13 @@ import { SleepWakeMessages } from './messages/index'; import { AcpMessageItem, AcpGroupedToolMessage, + AcpActivityGroupMessage, AcpTextMessage, } from './messages/acp'; +import { + buildAcpActivityRenderBlocks, + type AcpConversationRenderBlock, +} from './messages/acp/activity-groups'; import { buildAcpRenderBlocks, type AcpRenderBlock, @@ -120,7 +125,11 @@ function DebugTimestamp({ ); } -function blockUsesOwnTimestamp(block: AcpRenderBlock): boolean { +function blockUsesOwnTimestamp(block: AcpConversationRenderBlock): boolean { + if (block.kind === 'activity_group') { + return false; + } + if (block.kind === 'tool_group') { return false; } @@ -139,8 +148,14 @@ function blockUsesOwnTimestamp(block: AcpRenderBlock): boolean { } } -function hasVisibleAssistantOutput(blocks: AcpRenderBlock[]): boolean { +function hasVisibleAssistantOutput( + blocks: AcpConversationRenderBlock[], +): boolean { return blocks.some((block) => { + if (block.kind === 'activity_group') { + return hasVisibleAssistantOutput(block.blocks); + } + if (block.kind === 'tool_group') { return false; } @@ -211,24 +226,30 @@ const MessagesBase = ({ const resolvedHideFirstAcpUserPrompt = hideFirstAcpUserPrompt ?? shouldRenderSessionPrompt; - const renderBlocks = useMemo( - () => - buildAcpRenderBlocks(messages, { - displayMode: resolvedMessageUiOptions.displayMode, - initialPrompt: resolvedHideFirstAcpUserPrompt ? sessionPrompt : null, - shouldHideFirstMessage: resolvedHideFirstAcpUserPrompt, - showInternalMessages, - suppressedMessageIds, - }), - [ - messages, - resolvedHideFirstAcpUserPrompt, - resolvedMessageUiOptions.displayMode, - sessionPrompt, + const renderBlocks = useMemo(() => { + const acpBlocks = buildAcpRenderBlocks(messages, { + displayMode: resolvedMessageUiOptions.displayMode, + initialPrompt: resolvedHideFirstAcpUserPrompt ? sessionPrompt : null, + shouldHideFirstMessage: resolvedHideFirstAcpUserPrompt, showInternalMessages, suppressedMessageIds, - ], - ); + }); + + return buildAcpActivityRenderBlocks(acpBlocks, { + artifacts: session.artifacts, + displayMode: resolvedMessageUiOptions.displayMode, + hasLeadingTextBoundary: shouldRenderSessionPrompt, + }); + }, [ + messages, + resolvedHideFirstAcpUserPrompt, + resolvedMessageUiOptions.displayMode, + session.artifacts, + sessionPrompt, + shouldRenderSessionPrompt, + showInternalMessages, + suppressedMessageIds, + ]); const hasVisibleAssistantOutputInTranscript = hasVisibleAssistantOutput(renderBlocks); const shouldShowNarrationWorkingReasoning = @@ -244,25 +265,72 @@ const MessagesBase = ({ )); } - function renderRenderBlock(block: AcpRenderBlock, nested: boolean) { + function renderRenderBlock( + block: AcpConversationRenderBlock, + nested: boolean, + ) { const timestamp = showInternalMessages && !blockUsesOwnTimestamp(block) ? ( ) : null; + if (block.kind === 'activity_group') { + const content = ( + + {renderNestedBlocks(block.blocks)} + + ); + + const blockWithTimestamp = ( + <> + {content} + {timestamp} + + ); + + return nested ? ( + blockWithTimestamp + ) : ( + + {blockWithTimestamp} + + ); + } + if (block.kind === 'tool_group') { const content = ( { + if (excluded.has(anchorId)) { + return; + } + + excluded.add(anchorId); + anchorIds.push(anchorId); + }; + + collectBlockAnchorIdsInto(blocks, pushAnchor); + + return anchorIds; +} + +function collectBlockAnchorIdsInto( + blocks: AcpRenderBlock[], + pushAnchor: (anchorId: string) => void, +): void { + for (const block of blocks) { + if (block.kind === 'tool_group') { + pushAnchor(messageAnchorId(block.ts)); + for (const item of block.items) { + pushAnchor(messageAnchorId(item.msg.ts)); + } + continue; + } + + pushAnchor(messageAnchorId(block.msg.ts)); + + if (block.childBlocks) { + collectBlockAnchorIdsInto(block.childBlocks, pushAnchor); + } + } +} + +function blockHasPartialMessage(block: AcpRenderBlock): boolean { + if (block.kind === 'tool_group') { + return block.items.some((item) => item.msg.partial); + } + + return ( + block.msg.partial || + Boolean(block.childBlocks?.some(blockHasPartialMessage)) + ); +} diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/PendingEnvVarRequestPanel.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/PendingEnvVarRequestPanel.tsx index 7c292702..9b2586f3 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/PendingEnvVarRequestPanel.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/PendingEnvVarRequestPanel.tsx @@ -271,7 +271,7 @@ export function PendingEnvVarRequestPanel({ return (
-
+
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/PendingUserInputRequestCard.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/PendingUserInputRequestCard.tsx index c4fa7fad..21be3c25 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/PendingUserInputRequestCard.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/PendingUserInputRequestCard.tsx @@ -241,7 +241,7 @@ export function PendingUserInputRequestCard({ } return ( -
+
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/QueuedMessages.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/QueuedMessages.tsx index a56e877d..9ca67aa1 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/QueuedMessages.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/QueuedMessages.tsx @@ -209,7 +209,7 @@ export const QueuedMessagesContent = ({ } return ( -
+
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/TaskInputStack.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/TaskInputStack.tsx index f9275bfe..345d3389 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/TaskInputStack.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/TaskInputStack.tsx @@ -54,7 +54,7 @@ export function TaskInputStack({ /> {bootingTaskRun ? ( -
+
{ useSandboxTaskPhaseMock.mockReturnValue('running'); }); - it('opens populated todos by default', () => { + it('starts collapsed by default', () => { useSandboxTodosMock.mockReturnValue([ { id: '1', content: 'Inspect layout', status: 'in_progress' }, { id: '2', content: 'Move the todo block', status: 'pending' }, @@ -55,9 +55,17 @@ describe('TodoList', () => { expect( screen.getByRole('button', { name: /1 of 3 to-dos done/i }), ).toBeVisible(); + expect(screen.queryByText('Inspect layout')).not.toBeInTheDocument(); + expect(screen.queryByText('Move the todo block')).not.toBeInTheDocument(); + expect( + screen.queryByText('Verify prompt alignment'), + ).not.toBeInTheDocument(); + + fireEvent.click( + screen.getByRole('button', { name: /1 of 3 to-dos done/i }), + ); + expect(screen.getByText('Inspect layout')).toBeVisible(); - expect(screen.getByText('Move the todo block')).toBeVisible(); - expect(screen.getByText('Verify prompt alignment')).toBeVisible(); }); it('starts collapsed on mobile task entry', () => { @@ -93,7 +101,7 @@ describe('TodoList', () => { const { rerender } = render(); expect( - screen.getByRole('button', { name: /2 to-dos done/i }), + screen.getByRole('button', { name: /All 2 to-dos done/i }), ).toBeVisible(); expect(screen.queryByText('Inspect layout')).not.toBeInTheDocument(); @@ -127,52 +135,29 @@ describe('TodoList', () => { expect(container).toBeEmptyDOMElement(); }); - it('collapses when the last todo completes, then hides it after 10 seconds', () => { - vi.useFakeTimers(); - - try { - useSandboxTodosMock.mockReturnValue([ - { id: '1', content: 'Inspect layout', status: 'in_progress' }, - { id: '2', content: 'Move the todo block', status: 'completed' }, - ]); - - const { rerender } = render(); - - expect( - screen.getByRole('button', { name: /1 of 2 to-dos done/i }), - ).toBeVisible(); - expect(screen.getByText('Inspect layout')).toBeVisible(); - - useSandboxTodosMock.mockReturnValue([ - { id: '1', content: 'Inspect layout', status: 'completed' }, - { id: '2', content: 'Move the todo block', status: 'completed' }, - ]); - - rerender(); + it('collapses when the last todo completes and stays visible', () => { + useSandboxTodosMock.mockReturnValue([ + { id: '1', content: 'Inspect layout', status: 'in_progress' }, + { id: '2', content: 'Move the todo block', status: 'completed' }, + ]); - expect( - screen.getByRole('button', { name: /2 to-dos done/i }), - ).toBeVisible(); - expect(screen.queryByText('Inspect layout')).not.toBeInTheDocument(); + const { rerender } = render(); - act(() => { - vi.advanceTimersByTime(9_000); - }); + expect( + screen.getByRole('button', { name: /1 of 2 to-dos done/i }), + ).toBeVisible(); - expect( - screen.getByRole('button', { name: /2 to-dos done/i }), - ).toBeVisible(); + useSandboxTodosMock.mockReturnValue([ + { id: '1', content: 'Inspect layout', status: 'completed' }, + { id: '2', content: 'Move the todo block', status: 'completed' }, + ]); - act(() => { - vi.advanceTimersByTime(1_000); - }); + rerender(); - expect( - screen.queryByRole('button', { name: /2 to-dos done/i }), - ).not.toBeInTheDocument(); - } finally { - vi.useRealTimers(); - } + expect( + screen.getByRole('button', { name: /All 2 to-dos done/i }), + ).toBeVisible(); + expect(screen.queryByText('Inspect layout')).not.toBeInTheDocument(); }); it('does not present stale unfinished todos as active when the task is idle', () => { @@ -184,6 +169,10 @@ describe('TodoList', () => { const { container } = render(); + fireEvent.click( + screen.getByRole('button', { name: /0 of 2 to-dos done/i }), + ); + const items = Array.from(container.querySelectorAll('li')); expect(items).toHaveLength(2); @@ -201,6 +190,10 @@ describe('TodoList', () => { const { container } = render(); + fireEvent.click( + screen.getByRole('button', { name: /1 of 3 to-dos done/i }), + ); + const items = Array.from(container.querySelectorAll('li')); expect(items).toHaveLength(3); diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/TodoList.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/TodoList.tsx index 5d0ab3ea..acf1a6b8 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/TodoList.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/TodoList.tsx @@ -21,7 +21,6 @@ import { } from './hooks/SandboxProvider'; import { getDisplayedTodoStatus } from './todo-status'; -const HIDE_COMPLETED_TODOS_DELAY_MS = 10_000; const TODO_INACTIVE_TASK_PHASES: ReadonlySet = new Set([ 'idle', 'waiting_for_prompt', @@ -67,8 +66,7 @@ const TodoListContent = ({ autoCollapseKey, taskEntryKey }: TodoListProps) => { const completedCount = todos.filter((t) => t.status === 'completed').length; const allDone = todos.length > 0 && completedCount === todos.length; - const [isHiddenAfterCompletion, setIsHiddenAfterCompletion] = useState(false); - const [isOpen, setIsOpen] = useState(() => !allDone); + const [isOpen, setIsOpen] = useState(false); const autoCollapsedKeysRef = useRef(new Set()); const wasAllDoneRef = useRef(allDone); @@ -104,7 +102,6 @@ const TodoListContent = ({ autoCollapseKey, taskEntryKey }: TodoListProps) => { useEffect(() => { if (!allDone) { - setIsHiddenAfterCompletion(false); if (wasAllDoneRef.current) { setIsOpen(true); } @@ -114,22 +111,15 @@ const TodoListContent = ({ autoCollapseKey, taskEntryKey }: TodoListProps) => { wasAllDoneRef.current = true; setIsOpen(false); - setIsHiddenAfterCompletion(false); - - const hideTimer = window.setTimeout(() => { - setIsHiddenAfterCompletion(true); - }, HIDE_COMPLETED_TODOS_DELAY_MS); - - return () => window.clearTimeout(hideTimer); }, [allDone, todoStatusSignature]); - if (todos.length === 0 || isHiddenAfterCompletion) { + if (todos.length === 0) { return null; } return (
- + { diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.client.test.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.client.test.tsx index 3a6abe00..0be3d59d 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.client.test.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.client.test.tsx @@ -24,6 +24,8 @@ const { let capturedPlaceholder: string | undefined; let capturedSuggestion: unknown; let capturedSubmitWithMetaKey: boolean | undefined; +let capturedSubmitIcon: unknown; +let capturedSurface: string | undefined; const submittedFilesRef: { current: Array<{ url?: string; @@ -83,6 +85,8 @@ vi.mock('@/components/tasks', () => ({ placeholder, suggestion, submitWithMetaKey, + submitIcon, + surface, }: { isBusy: boolean; promptText: string; @@ -98,10 +102,14 @@ vi.mock('@/components/tasks', () => ({ placeholder?: string; suggestion?: unknown; submitWithMetaKey?: boolean; + submitIcon?: unknown; + surface?: string; }) => { capturedPlaceholder = placeholder; capturedSuggestion = suggestion; capturedSubmitWithMetaKey = submitWithMetaKey; + capturedSubmitIcon = submitIcon; + capturedSurface = surface; return (
@@ -140,6 +148,8 @@ describe('WakeTaskInput', () => { capturedPlaceholder = undefined; capturedSuggestion = undefined; capturedSubmitWithMetaKey = undefined; + capturedSubmitIcon = undefined; + capturedSurface = undefined; submittedFilesRef.current = []; preparePromptAttachmentsMock.mockResolvedValue({ text: 'Wake up and keep going', @@ -152,6 +162,47 @@ describe('WakeTaskInput', () => { useSandboxCurrentUserInfoMock.mockReturnValue(null); }); + it('shows a wake icon until the user types a message', () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + + renderWithQueryClient( + , + queryClient, + ); + + expect(capturedSubmitIcon).toBeTruthy(); + + fireEvent.change(screen.getByLabelText('Wake prompt'), { + target: { value: 'keep going' }, + }); + + expect(capturedSubmitIcon).toBeUndefined(); + }); + + it('uses an embedded prompt surface when rendered inside a tray', () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }); + + renderWithQueryClient( + , + queryClient, + ); + + expect(capturedSurface).toBe('embedded'); + }); + it('prefills the sleeping draft, appends an optimistic transcript row, and resumes the task with a deferred prompt', async () => { const queryClient = new QueryClient({ defaultOptions: { @@ -209,7 +260,7 @@ describe('WakeTaskInput', () => { }), ]); expect(toastErrorMock).not.toHaveBeenCalled(); - expect(capturedPlaceholder).toBe('Wake up Roomote with this message'); + expect(capturedPlaceholder).toBe('Wake up Roomote with a message...'); expect(capturedSuggestion).toBeUndefined(); expect(capturedSubmitWithMetaKey).toBe(false); }); diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.tsx index fff0f555..36b1d3af 100644 --- a/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.tsx +++ b/apps/web/src/app/(sandbox)/task/[taskId]/WakeTaskInput.tsx @@ -5,20 +5,24 @@ import { toast } from 'sonner'; import type { PromptInputMessage } from '@/components/ai-elements'; import { TaskPromptInput } from '@/components/tasks'; +import { Sun } from '@/components/system'; import { useRestoreTaskRunSnapshot } from '@/hooks/snapshots'; import { preparePromptAttachments } from '@/lib/prompt-attachments'; import type { TaskRunDetail } from '@/lib/server'; +import { cn } from '@/lib/utils'; import { useOptimisticPromptSubmission } from './prompt-input/useOptimisticPromptSubmission'; interface WakeTaskInputProps { taskRun: Pick; initialPrompt?: string; + embedded?: boolean; } export function WakeTaskInput({ taskRun, initialPrompt = '', + embedded = false, }: WakeTaskInputProps) { const { rollbackOptimisticPromptSubmission, @@ -115,19 +119,26 @@ export function WakeTaskInput({ } }; - return ( -
-
- -
+ const input = ( +
+ : undefined} + surface={embedded ? 'embedded' : 'default'} + />
); + + return embedded ? input :
{input}
; } diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpActivityGroupMessage.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpActivityGroupMessage.tsx new file mode 100644 index 00000000..16062174 --- /dev/null +++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpActivityGroupMessage.tsx @@ -0,0 +1,81 @@ +'use client'; + +import type { ReactNode } from 'react'; + +import { + ChevronRight, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/system'; +import { cn } from '@/lib/utils'; + +import type { AcpActivityGroupRenderBlock } from './activity-groups'; + +interface AcpActivityGroupMessageProps { + group: AcpActivityGroupRenderBlock; + anchorIds?: string[]; + children: ReactNode; +} + +export function AcpActivityGroupMessage({ + group, + anchorIds = [], + children, +}: AcpActivityGroupMessageProps) { + return ( + + {anchorIds.map((anchorId) => ( +