mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 00:03:37 +00:00
fix(activity): cap assistant preview rendering
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
import type { Repo, TerminalTab, Worktree } from '../../../../shared/types'
|
||||
import type { RetainedAgentEntry } from '@/store/slices/agent-status'
|
||||
import {
|
||||
activityThreadResponseRenderPreview,
|
||||
activityThreadMatchesSearchQuery,
|
||||
buildActivityEvents,
|
||||
buildAgentPaneThreads
|
||||
@@ -98,7 +99,8 @@ function makeRetainedDoneEntry(tab: TerminalTab): RetainedAgentEntry {
|
||||
paneKey: 'tab-1:1',
|
||||
terminalTitle: 'Claude',
|
||||
stateHistory: [],
|
||||
agentType: 'claude'
|
||||
agentType: 'claude',
|
||||
lastAssistantMessage: 'Retained response preview'
|
||||
},
|
||||
worktreeId: 'wt-1',
|
||||
tab,
|
||||
@@ -107,23 +109,42 @@ function makeRetainedDoneEntry(tab: TerminalTab): RetainedAgentEntry {
|
||||
}
|
||||
}
|
||||
|
||||
function makeActivityResult(args: {
|
||||
entries?: Record<string, AgentStatusEntry>
|
||||
retained?: Record<string, RetainedAgentEntry>
|
||||
tab?: TerminalTab
|
||||
now?: number
|
||||
}): ReturnType<typeof buildActivityEvents> {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
const tab = args.tab ?? makeTab()
|
||||
|
||||
return buildActivityEvents({
|
||||
agentStatusByPaneKey: args.entries ?? {},
|
||||
retainedAgentsByPaneKey: args.retained ?? {},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: args.now ?? 3_000
|
||||
})
|
||||
}
|
||||
|
||||
function makeThreads(result: ReturnType<typeof buildActivityEvents>) {
|
||||
return buildAgentPaneThreads({
|
||||
events: result.events,
|
||||
liveAgentByPaneKey: result.liveAgentByPaneKey
|
||||
})
|
||||
}
|
||||
|
||||
describe('buildActivityEvents', () => {
|
||||
it('keeps a prior done event after the same pane starts working again', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
const tab = makeTab()
|
||||
|
||||
const result = buildActivityEvents({
|
||||
agentStatusByPaneKey: {
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': makeWorkingEntryWithPriorDone()
|
||||
},
|
||||
retainedAgentsByPaneKey: {},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: 2_000
|
||||
})
|
||||
|
||||
@@ -136,10 +157,7 @@ describe('buildActivityEvents', () => {
|
||||
expect(result.liveAgentByPaneKey['tab-1:1'].state).toBe('working')
|
||||
expect(result.liveAgentByPaneKey['tab-1:1'].entry.prompt).toBe('Second prompt')
|
||||
|
||||
const threads = buildAgentPaneThreads({
|
||||
events: result.events,
|
||||
liveAgentByPaneKey: result.liveAgentByPaneKey
|
||||
})
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(threads).toHaveLength(1)
|
||||
expect(threads[0].paneTitle).toBe('Second prompt')
|
||||
@@ -148,21 +166,10 @@ describe('buildActivityEvents', () => {
|
||||
})
|
||||
|
||||
it('does not keep showing a stale live agent as running', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
const tab = makeTab()
|
||||
|
||||
const result = buildActivityEvents({
|
||||
agentStatusByPaneKey: {
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': makeWorkingEntryWithPriorDone()
|
||||
},
|
||||
retainedAgentsByPaneKey: {},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: 2_000 + AGENT_STATUS_STALE_AFTER_MS + 1
|
||||
})
|
||||
|
||||
@@ -171,28 +178,13 @@ describe('buildActivityEvents', () => {
|
||||
})
|
||||
|
||||
it('creates a thread for a fresh running agent with no historical events', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
const tab = makeTab()
|
||||
|
||||
const result = buildActivityEvents({
|
||||
agentStatusByPaneKey: {
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': makeWorkingEntryWithoutHistory()
|
||||
},
|
||||
retainedAgentsByPaneKey: {},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: 3_000
|
||||
}
|
||||
})
|
||||
|
||||
const threads = buildAgentPaneThreads({
|
||||
events: result.events,
|
||||
liveAgentByPaneKey: result.liveAgentByPaneKey
|
||||
})
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(result.events).toHaveLength(0)
|
||||
expect(threads).toHaveLength(1)
|
||||
@@ -207,32 +199,20 @@ describe('buildActivityEvents', () => {
|
||||
})
|
||||
|
||||
it('matches a custom-titled live thread by its current prompt', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
const tab = { ...makeTab(), customTitle: 'Pinned agent title' }
|
||||
const entry = {
|
||||
...makeWorkingEntryWithoutHistory(),
|
||||
prompt: 'Investigate activity live prompt search'
|
||||
}
|
||||
|
||||
const result = buildActivityEvents({
|
||||
agentStatusByPaneKey: {
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': entry
|
||||
},
|
||||
retainedAgentsByPaneKey: {},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: 3_000
|
||||
tab
|
||||
})
|
||||
|
||||
const threads = buildAgentPaneThreads({
|
||||
events: result.events,
|
||||
liveAgentByPaneKey: result.liveAgentByPaneKey
|
||||
})
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(threads[0].paneTitle).toBe('Pinned agent title')
|
||||
expect(
|
||||
@@ -243,25 +223,93 @@ describe('buildActivityEvents', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('overlays fresh live state onto retained-only activity for a reused pane key', () => {
|
||||
const repo = makeRepo()
|
||||
const worktree = makeWorktree()
|
||||
it('surfaces the current live assistant response as the thread preview', () => {
|
||||
const entry = {
|
||||
...makeWorkingEntryWithoutHistory(),
|
||||
lastAssistantMessage: 'I updated the tests and checked the activity row.'
|
||||
}
|
||||
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': entry
|
||||
}
|
||||
})
|
||||
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(threads[0].responsePreview).toBe('I updated the tests and checked the activity row.')
|
||||
expect(
|
||||
activityThreadMatchesSearchQuery({
|
||||
thread: threads[0],
|
||||
searchQuery: 'checked the activity row'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('caps rendered assistant response preview without changing searchable thread text', () => {
|
||||
const longResponse = `${'Preview details '.repeat(80)}activity row searchable tail`
|
||||
const entry = {
|
||||
...makeWorkingEntryWithoutHistory(),
|
||||
lastAssistantMessage: longResponse
|
||||
}
|
||||
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': entry
|
||||
}
|
||||
})
|
||||
|
||||
const threads = makeThreads(result)
|
||||
const renderedPreview = activityThreadResponseRenderPreview({
|
||||
responsePreview: threads[0].responsePreview
|
||||
})
|
||||
|
||||
expect(renderedPreview.length).toBeLessThan(longResponse.length)
|
||||
expect(renderedPreview.endsWith('...')).toBe(true)
|
||||
expect(
|
||||
activityThreadMatchesSearchQuery({
|
||||
thread: threads[0],
|
||||
searchQuery: 'searchable tail'
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not leave a lone surrogate when capping the rendered response preview', () => {
|
||||
const renderedPreview = activityThreadResponseRenderPreview({
|
||||
responsePreview: `${'a'.repeat(319)}😀tail`
|
||||
})
|
||||
const beforeEllipsis = renderedPreview.slice(0, -3)
|
||||
const lastCode = beforeEllipsis.charCodeAt(beforeEllipsis.length - 1)
|
||||
|
||||
expect(lastCode >= 0xd800 && lastCode <= 0xdbff).toBe(false)
|
||||
})
|
||||
|
||||
it('surfaces the retained done assistant response as the thread preview', () => {
|
||||
const tab = makeTab()
|
||||
|
||||
const result = buildActivityEvents({
|
||||
agentStatusByPaneKey: {
|
||||
'tab-1:1': makeWorkingEntryWithoutHistory()
|
||||
},
|
||||
retainedAgentsByPaneKey: {
|
||||
const result = makeActivityResult({
|
||||
retained: {
|
||||
'tab-1:1': makeRetainedDoneEntry(tab)
|
||||
},
|
||||
tabsByWorktree: {
|
||||
[worktree.id]: [tab]
|
||||
tab
|
||||
})
|
||||
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(threads[0].responsePreview).toBe('Retained response preview')
|
||||
})
|
||||
|
||||
it('overlays fresh live state onto retained-only activity for a reused pane key', () => {
|
||||
const tab = makeTab()
|
||||
|
||||
const result = makeActivityResult({
|
||||
entries: {
|
||||
'tab-1:1': makeWorkingEntryWithoutHistory()
|
||||
},
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[repo.id, repo]]),
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
now: 3_000
|
||||
retained: {
|
||||
'tab-1:1': makeRetainedDoneEntry(tab)
|
||||
},
|
||||
tab
|
||||
})
|
||||
|
||||
expect(result.events).toHaveLength(1)
|
||||
@@ -272,13 +320,11 @@ describe('buildActivityEvents', () => {
|
||||
expect(result.events[0].entry.prompt).toBe('Retained prior run')
|
||||
expect(result.liveAgentByPaneKey['tab-1:1'].state).toBe('working')
|
||||
|
||||
const threads = buildAgentPaneThreads({
|
||||
events: result.events,
|
||||
liveAgentByPaneKey: result.liveAgentByPaneKey
|
||||
})
|
||||
const threads = makeThreads(result)
|
||||
|
||||
expect(threads).toHaveLength(1)
|
||||
expect(threads[0].paneTitle).toBe('New run')
|
||||
expect(threads[0].responsePreview).toBe('')
|
||||
expect(threads[0].latestTimestamp).toBe(3_000)
|
||||
expect(threads[0].events[0].entry.prompt).toBe('Retained prior run')
|
||||
})
|
||||
|
||||
@@ -29,8 +29,10 @@ import { useSidebarResize } from '@/hooks/useSidebarResize'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -38,6 +40,7 @@ import { Toggle } from '@/components/ui/toggle'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { FilledBellIcon } from '../sidebar/WorktreeCardHelpers'
|
||||
import CommentMarkdown from '../sidebar/CommentMarkdown'
|
||||
import {
|
||||
setActivityTerminalPortals,
|
||||
type ActivityTerminalPortalTarget
|
||||
@@ -90,6 +93,7 @@ type AgentPaneThread = {
|
||||
agentType: AgentType
|
||||
currentAgentState: ActivityLiveAgentState | null
|
||||
currentAgentEntry: AgentStatusEntry | null
|
||||
responsePreview: string
|
||||
latestTimestamp: number
|
||||
latestEvent: ActivityEvent | null
|
||||
events: ActivityEvent[]
|
||||
@@ -110,6 +114,7 @@ type ActivityTerminalPortalDomStatus = {
|
||||
type ActivityTerminalPortalSlotId = 'primary' | 'secondary'
|
||||
|
||||
const ACTIVITY_TERMINAL_LOADING_LABEL_DELAY_MS = 180
|
||||
const ACTIVITY_THREAD_RESPONSE_RENDER_PREVIEW_MAX_LENGTH = 320
|
||||
|
||||
const absoluteDateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
@@ -139,6 +144,33 @@ function formatRelativeTime(timestamp: number): string {
|
||||
return relativeTimeFormatter.format(diffDays, 'day')
|
||||
}
|
||||
|
||||
function truncatePreservingSurrogates(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) {
|
||||
return value
|
||||
}
|
||||
const truncated = value.slice(0, maxLength)
|
||||
const lastCode = truncated.charCodeAt(truncated.length - 1)
|
||||
if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
|
||||
return truncated.slice(0, -1)
|
||||
}
|
||||
return truncated
|
||||
}
|
||||
|
||||
export function activityThreadResponseRenderPreview({
|
||||
responsePreview
|
||||
}: {
|
||||
responsePreview: string
|
||||
}): string {
|
||||
const trimmed = responsePreview.trim()
|
||||
if (trimmed.length <= ACTIVITY_THREAD_RESPONSE_RENDER_PREVIEW_MAX_LENGTH) {
|
||||
return trimmed
|
||||
}
|
||||
return `${truncatePreservingSurrogates(
|
||||
trimmed,
|
||||
ACTIVITY_THREAD_RESPONSE_RENDER_PREVIEW_MAX_LENGTH
|
||||
).trimEnd()}...`
|
||||
}
|
||||
|
||||
function paneIdFromPaneKey(paneKey: string): number | null {
|
||||
const colon = paneKey.indexOf(':')
|
||||
const tail = colon > 0 ? paneKey.slice(colon + 1) : ''
|
||||
@@ -329,6 +361,10 @@ function paneTitleForEvent(event: ActivityEvent): string {
|
||||
return paneTitleForEntry(event.entry, event.tab)
|
||||
}
|
||||
|
||||
function responsePreviewForEntry(entry: AgentStatusEntry): string {
|
||||
return entry.lastAssistantMessage?.trim() ?? ''
|
||||
}
|
||||
|
||||
function isActivityEventState(state: AgentStatusState): state is ActivityEventState {
|
||||
return state === 'done' || state === 'blocked' || state === 'waiting'
|
||||
}
|
||||
@@ -554,6 +590,7 @@ export function buildAgentPaneThreads(args: {
|
||||
agentType: event.agentType,
|
||||
currentAgentState: null,
|
||||
currentAgentEntry: null,
|
||||
responsePreview: responsePreviewForEntry(event.entry),
|
||||
latestTimestamp: event.timestamp,
|
||||
latestEvent: event,
|
||||
events: [event],
|
||||
@@ -568,6 +605,7 @@ export function buildAgentPaneThreads(args: {
|
||||
existing.paneTitle = paneTitleForEvent(event)
|
||||
existing.agentType = event.agentType
|
||||
existing.tab = event.tab
|
||||
existing.responsePreview = responsePreviewForEntry(event.entry)
|
||||
existing.latestTimestamp = event.timestamp
|
||||
}
|
||||
}
|
||||
@@ -584,6 +622,7 @@ export function buildAgentPaneThreads(args: {
|
||||
agentType: liveAgent.agentType,
|
||||
currentAgentState: liveAgent.state,
|
||||
currentAgentEntry: liveAgent.entry,
|
||||
responsePreview: responsePreviewForEntry(liveAgent.entry),
|
||||
latestTimestamp: liveAgent.timestamp,
|
||||
latestEvent: null,
|
||||
events: [],
|
||||
@@ -601,6 +640,7 @@ export function buildAgentPaneThreads(args: {
|
||||
existing.agentType = liveAgent.agentType
|
||||
existing.currentAgentState = liveAgent.state
|
||||
existing.currentAgentEntry = liveAgent.entry
|
||||
existing.responsePreview = responsePreviewForEntry(liveAgent.entry)
|
||||
existing.latestTimestamp = liveAgent.timestamp
|
||||
}
|
||||
|
||||
@@ -667,7 +707,7 @@ function threadSearchText(thread: AgentPaneThread): string {
|
||||
const latestEventText = latest
|
||||
? `${agentTitle(latest)} ${agentSummary(latest)} ${agentMeta(latest)}`
|
||||
: ''
|
||||
return `${thread.paneTitle} ${thread.worktree.displayName} ${thread.repo?.displayName ?? ''} ${formatAgentTypeLabel(thread.agentType)} ${stateLabel} ${currentPrompt} ${currentSummary} ${latestEventText}`.toLowerCase()
|
||||
return `${thread.paneTitle} ${thread.worktree.displayName} ${thread.repo?.displayName ?? ''} ${formatAgentTypeLabel(thread.agentType)} ${stateLabel} ${currentPrompt} ${currentSummary} ${thread.responsePreview} ${latestEventText}`.toLowerCase()
|
||||
}
|
||||
|
||||
export function activityThreadMatchesSearchQuery({
|
||||
@@ -698,19 +738,41 @@ function ThreadAgentStateIndicator({ thread }: { thread: AgentPaneThread }): Rea
|
||||
)
|
||||
}
|
||||
|
||||
function isEventFromNestedInteractiveElement(
|
||||
target: EventTarget | null,
|
||||
currentTarget: HTMLElement
|
||||
): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false
|
||||
}
|
||||
const interactiveTarget = target.closest(
|
||||
'a, button, input, select, textarea, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])'
|
||||
)
|
||||
return (
|
||||
interactiveTarget instanceof HTMLElement &&
|
||||
interactiveTarget !== currentTarget &&
|
||||
currentTarget.contains(interactiveTarget)
|
||||
)
|
||||
}
|
||||
|
||||
function ThreadRow({
|
||||
thread,
|
||||
selected,
|
||||
onSelect,
|
||||
onJump,
|
||||
onMarkUnread
|
||||
onMarkUnread,
|
||||
compactMode
|
||||
}: {
|
||||
thread: AgentPaneThread
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
onJump: () => void
|
||||
onMarkUnread: () => void
|
||||
compactMode: boolean
|
||||
}): React.JSX.Element {
|
||||
const renderedResponsePreview = activityThreadResponseRenderPreview({
|
||||
responsePreview: thread.responsePreview
|
||||
})
|
||||
return (
|
||||
<div
|
||||
data-current={selected ? 'true' : undefined}
|
||||
@@ -718,6 +780,11 @@ function ThreadRow({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
// Why: response markdown can contain links; keyboard activation on a
|
||||
// nested link should follow the link instead of selecting the row.
|
||||
if (isEventFromNestedInteractiveElement(event.target, event.currentTarget)) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onSelect()
|
||||
@@ -757,14 +824,32 @@ function ThreadRow({
|
||||
<AgentIcon agent={agentTypeToIconAgent(thread.agentType)} size={14} />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'line-clamp-3 min-w-0 flex-1 break-words text-xs leading-snug',
|
||||
thread.unread ? 'font-semibold text-foreground' : 'font-medium text-foreground'
|
||||
)}
|
||||
>
|
||||
{thread.paneTitle}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 text-xs leading-snug',
|
||||
compactMode ? 'block truncate' : 'line-clamp-3 break-words',
|
||||
thread.unread ? 'font-semibold text-foreground' : 'font-medium text-foreground'
|
||||
)}
|
||||
title={compactMode ? thread.paneTitle : undefined}
|
||||
>
|
||||
{thread.paneTitle}
|
||||
</span>
|
||||
{!compactMode && renderedResponsePreview ? (
|
||||
<CommentMarkdown
|
||||
content={renderedResponsePreview}
|
||||
className={cn(
|
||||
// Why: mirror the in-workspace agent card's compact response
|
||||
// preview while keeping Activity rows to one scannable line;
|
||||
// the content is capped before markdown parsing to keep large
|
||||
// assistant summaries cheap in long Activity lists.
|
||||
'mt-1 h-[1lh] min-w-0 overflow-hidden truncate whitespace-nowrap text-[11px] font-normal leading-snug text-muted-foreground/80',
|
||||
'[&_*]:inline [&_*]:!m-0 [&_*]:!p-0 [&_*]:!whitespace-nowrap [&_br]:hidden [&_ol]:list-none [&_ul]:list-none'
|
||||
)}
|
||||
title={thread.responsePreview}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="inline-flex shrink-0 items-center gap-1.5 pt-px">
|
||||
{/* Why (bell matches WorktreeCard pattern): unread → amber filled
|
||||
bell as a static, non-interactive cue (selecting the thread
|
||||
@@ -850,6 +935,7 @@ function ThreadRow({
|
||||
export default function ActivityPrototypePage(): React.JSX.Element {
|
||||
const [readFilter, setReadFilter] = useState<ThreadReadFilter>('all')
|
||||
const [query, setQuery] = useState('')
|
||||
const [compactMode, setCompactMode] = useState(false)
|
||||
const [selectedPaneKey, setSelectedPaneKey] = useState<string | null>(null)
|
||||
const [displayedPaneKey, setDisplayedPaneKey] = useState<string | null>(null)
|
||||
const [activePortalSlotId, setActivePortalSlotId] =
|
||||
@@ -1200,6 +1286,14 @@ export default function ActivityPrototypePage(): React.JSX.Element {
|
||||
<TooltipContent side="bottom">More options</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" sideOffset={6}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={compactMode}
|
||||
onCheckedChange={(checked) => setCompactMode(checked === true)}
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
>
|
||||
Compact mode
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => markAllThreadsRead()}
|
||||
disabled={!hasUnreadThreads}
|
||||
@@ -1219,6 +1313,7 @@ export default function ActivityPrototypePage(): React.JSX.Element {
|
||||
onSelect={() => selectThread(thread)}
|
||||
onJump={() => jumpToWorkspace(thread)}
|
||||
onMarkUnread={() => markThreadUnread(thread)}
|
||||
compactMode={compactMode}
|
||||
/>
|
||||
))}
|
||||
{visibleThreads.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user