diff --git a/docs/ui-interaction-and-motion.md b/docs/ui-interaction-and-motion.md index 0b80e6090..997824752 100644 --- a/docs/ui-interaction-and-motion.md +++ b/docs/ui-interaction-and-motion.md @@ -98,8 +98,14 @@ retryable errors. Without a Workspace, only the Harness header remains: clicking it opens the existing setup landing flow, without creating or selecting a Workspace. Do not repeat setup copy or a second setup button below it. Before readiness, the new-research shortcut is hidden. -Studio is a compact outlined tool button with route-owned selection, separate -from conversation rows; Quant and Prediction share its presentation. +Studio is a quiet, borderless child navigation row aligned with Sessions, with +route-owned selection. Its arrow appears on hover or keyboard focus and remains +visible on touch devices; Quant and Prediction share its presentation. +`SidebarChildRow` and `SidebarChildRowButton` own Harness child geometry for +both Studio and Sessions: a 16px icon slot, 8px label gap, shared selection and +keyboard focus, and sibling action controls. Expanded fine-pointer desktop rows +are 30px tall with no additional per-destination vertical padding; other surfaces +retain the existing Session row density. Keep runtime behavior in the caller. Harness working views use one content top bar, not a second conversation sidebar. TerminalView has no card/canvas mode: its header always uses PageTopBar and its single grid row fills the remaining height. Do not reserve a local header row @@ -172,6 +178,14 @@ the Harness launch page; its context, controls and details are caller-owned slots, not embedded Pi selectors. Existing `oa-harness-composer-*` styling seams remain the shared visual material. Messages and composer use a 46rem reading measure, with local scrolling for wide output and wrapping toolbar controls. +User messages use a quiet, borderless bubble; assistant prose sits directly on +the canvas. Execution summaries are lightweight disclosure rows, with an inset +rail for individual actions rather than nested activity cards. Preserve the +shared Markdown table scroll wrapper instead of overriding table display. +Completed text has a copy action that copies only the displayed message, not +reasoning or tool payloads. Older actions reveal on hover or keyboard focus; +the latest message and touch surfaces keep them visible. Clipboard failures +are actionable, and interrupted tools must say incomplete rather than completed. The normalized types in this folder are ephemeral presentation data, not a new persisted transcript or execution protocol. An adapter converts wire messages diff --git a/ui/src/components/ActivityBar.tsx b/ui/src/components/ActivityBar.tsx index e6208f2b7..0f629386d 100644 --- a/ui/src/components/ActivityBar.tsx +++ b/ui/src/components/ActivityBar.tsx @@ -197,7 +197,7 @@ export function ActivityBar({ badge = { count: unreadInbox, label: t('nav.unread', { count: unreadInbox }), - tone: 'bg-sidebar-foreground text-sidebar', + tone: 'oa-inbox-unread-count', } } else if (item.page === 'portfolio' && pendingPush > 0) { badge = { diff --git a/ui/src/components/InboxSidebar.tsx b/ui/src/components/InboxSidebar.tsx index b66aee627..3f3623946 100644 --- a/ui/src/components/InboxSidebar.tsx +++ b/ui/src/components/InboxSidebar.tsx @@ -323,7 +323,7 @@ function WorkspaceView({ {workspaceLabel} {unread > 0 && ( - + {unread} )} @@ -393,7 +393,7 @@ function ClusterRow({ {active && } { + const open = vi.fn() + const options = vi.fn() + render( + } onClick={open} aria-current="page">Studio + + ) + const main = screen.getByRole('button', { name: 'Studio' }) + expect(main.getAttribute('aria-current')).toBe('page') + expect(main.parentElement?.dataset.active).toBe('true') + expect(main.querySelector('span')?.className).toContain('size-4') + fireEvent.click(screen.getByRole('button', { name: 'Options' })) + expect(open).not.toHaveBeenCalled() + fireEvent.click(main) + expect(open).toHaveBeenCalledOnce() + expect(options).toHaveBeenCalledOnce() +}) diff --git a/ui/src/components/SidebarChildRow.tsx b/ui/src/components/SidebarChildRow.tsx new file mode 100644 index 000000000..ef3a6200d --- /dev/null +++ b/ui/src/components/SidebarChildRow.tsx @@ -0,0 +1,19 @@ +import type { ComponentProps, ReactNode } from 'react' +import { SelectionIndicator } from './SelectionIndicator' + +/** Shared geometry for Harness child destinations; actions are sibling controls. */ +export function SidebarChildRow({ active, className = '', children, ...props }: ComponentProps<'div'> & { active: boolean }) { + return
+ {active && } + {children} +
+} + +export function SidebarChildRowButton({ icon, children, className = '', ...props }: ComponentProps<'button'> & { icon: ReactNode }) { + return +} diff --git a/ui/src/components/conversation/ConversationTranscript.tsx b/ui/src/components/conversation/ConversationTranscript.tsx index b177d68f5..8f5ce746d 100644 --- a/ui/src/components/conversation/ConversationTranscript.tsx +++ b/ui/src/components/conversation/ConversationTranscript.tsx @@ -2,18 +2,22 @@ import { useEffect, useState, type ReactElement } from 'react' import { Check, ChevronRight, CircleAlert, CircleDashed, LoaderCircle } from 'lucide-react' import { MarkdownContent } from '../MarkdownContent' import type { ConversationActivity, ConversationContent, ConversationItem, ConversationToolStep } from './types' +import { MessageActions } from './MessageActions' export function ConversationTranscriptItem({ item, working, + latest = false, }: { readonly item: ConversationItem readonly working: boolean + readonly latest?: boolean }): ReactElement { if (item.kind === 'user') { return ( -
+
+ {item.content.some(block => block.kind === 'markdown') && block.kind === 'markdown' ? [block.text] : []).join('\n\n')} />}
) } @@ -32,7 +36,7 @@ export function ConversationTranscriptItem({ ) } return ( -
+
{item.progress.map((text, index) => (
@@ -40,6 +44,7 @@ export function ConversationTranscriptItem({ {item.activity && } {item.final &&
}
+ {!working && item.final && }
) } @@ -122,7 +127,7 @@ function ConversationToolStepView({ step, working }: { readonly step: Conversati : } {step.name} - {summary ?? (step.status === 'running' ? 'Running…' : 'Completed')} + {summary ?? (step.status === 'running' ? (working ? 'Running…' : 'Incomplete') : failed ? 'Failed' : 'Completed')} {resultChars !== null && {formatChars(resultChars)}}