Repository navigation
feat(frontend): composer layout refresh and activity scanner under the composer #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
332fb07
feat(frontend): move session activity scanner under the composer
76d52b3
feat(frontend): move send to the input and show key bar in the contro…
6487132
feat(frontend): consolidate composer controls at 36px and repaint the…
chriswritescode-dev 32102cc
fix(frontend): anchor the mobile activity scanner just below the comp…
chriswritescode-dev 570e528
feat(frontend): dock the mobile composer as a solid full-width strip …
chriswritescode-dev 1607703
fix(frontend): lift the mobile composer row clear of the rounded scre…
chriswritescode-dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
frontend/src/components/message/ComposerActivityBar.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { render, screen } from '@testing-library/react' | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest' | ||
| import { ComposerActivityBar } from './ComposerActivityBar' | ||
| import { useSessionStatus } from '@/stores/sessionStatusStore' | ||
| import { stubMatchMedia } from '@/test/test-utils' | ||
|
|
||
| const originalMatchMedia = window.matchMedia | ||
|
|
||
| const resetStore = () => { | ||
| useSessionStatus.setState({ | ||
| statuses: new Map(), | ||
| statusCache: new Map(), | ||
| statusRevisions: new Map(), | ||
| knownSessions: new Set(), | ||
| outcomes: new Map(), | ||
| parents: new Map(), | ||
| directories: new Map(), | ||
| runningShells: new Map(), | ||
| revision: 0, | ||
| }) | ||
| } | ||
|
|
||
| const trackOf = (bar: HTMLElement) => bar.firstElementChild as HTMLElement | ||
|
|
||
| describe('ComposerActivityBar', () => { | ||
| beforeEach(() => { | ||
| resetStore() | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| Object.defineProperty(window, 'matchMedia', { | ||
| configurable: true, | ||
| writable: true, | ||
| value: originalMatchMedia, | ||
| }) | ||
| }) | ||
|
|
||
| it('renders nothing when the session is idle without background activity', () => { | ||
| const { container } = render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| expect(container).toBeEmptyDOMElement() | ||
| }) | ||
|
|
||
| it('sits inside the docked composer on mobile and below the composer on desktop', () => { | ||
| useSessionStatus.getState().setStatus('session-a', { type: 'busy' }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const bar = screen.getByRole('progressbar', { name: 'Session working' }) | ||
| expect(bar).toHaveClass('pointer-events-none', 'flex') | ||
| expect(bar).toHaveClass('absolute', 'inset-x-3', 'bottom-4', 'md:-bottom-[11px]') | ||
| expect(bar).not.toHaveClass('fixed') | ||
| }) | ||
|
|
||
| it('renders a masked segment track with only the sweeping window', () => { | ||
| useSessionStatus.getState().setStatus('session-a', { type: 'busy' }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Session working' })) | ||
| expect(track).toHaveClass('scanner-track', 'relative', 'h-2', 'w-full') | ||
|
|
||
| const children = Array.from(track.children) as HTMLElement[] | ||
| expect(children).toHaveLength(1) | ||
| expect(children[0]).toHaveClass('scanner-sweep') | ||
| }) | ||
|
|
||
| it('tints the track with the info tone while the session is busy', () => { | ||
| useSessionStatus.getState().setStatus('session-a', { type: 'busy' }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Session working' })) | ||
| expect(track).toHaveClass('text-info') | ||
| }) | ||
|
|
||
| it('tints the track with the primary tone while compacting', () => { | ||
| useSessionStatus.getState().setStatus('session-a', { type: 'compact' }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Compacting' })) | ||
| expect(track).toHaveClass('text-primary') | ||
| }) | ||
|
|
||
| it('reports the retry attempt in the label with the warning tone', () => { | ||
| useSessionStatus.getState().setStatus('session-a', { | ||
| type: 'retry', | ||
| attempt: 2, | ||
| message: 'rate limited', | ||
| next: Date.now() + 5_000, | ||
| }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Retrying (Retry #2)' })) | ||
| expect(track).toHaveClass('text-warning') | ||
| }) | ||
|
|
||
| it('tints the track with the muted foreground tone for background activity', () => { | ||
| useSessionStatus.getState().setShellRunning('shell-1', 'session-a', '/repo') | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Background tasks running' })) | ||
| expect(track).toHaveClass('text-muted-foreground') | ||
| }) | ||
|
|
||
| it('keeps sweeping when reduced motion is requested', () => { | ||
| stubMatchMedia(true) | ||
| useSessionStatus.getState().setStatus('session-a', { type: 'busy' }) | ||
|
|
||
| render(<ComposerActivityBar sessionID="session-a" />) | ||
|
|
||
| const track = trackOf(screen.getByRole('progressbar', { name: 'Session working' })) | ||
| expect(track.querySelector('.scanner-sweep')).not.toBeNull() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { ScannerBars, type ScannerTone } from '@/components/ui/scanner-bars' | ||
| import { useHasBackgroundActivity, useSessionStatusForSession } from '@/stores/sessionStatusStore' | ||
|
|
||
| interface ComposerActivityBarProps { | ||
| sessionID: string | ||
| } | ||
|
|
||
| const ACTIVITY_PRESENTATION = { | ||
| background: { | ||
| label: 'Background tasks running', | ||
| tone: 'muted-foreground', | ||
| }, | ||
| busy: { | ||
| label: 'Session working', | ||
| tone: 'info', | ||
| }, | ||
| compact: { | ||
| label: 'Compacting', | ||
| tone: 'primary', | ||
| }, | ||
| retry: { | ||
| label: 'Retrying', | ||
| tone: 'warning', | ||
| }, | ||
| } as const satisfies Record<string, { label: string; tone: ScannerTone }> | ||
|
|
||
| /** | ||
| * Session activity scanner shown by the composer. On mobile it sits inside the bottom edge of the | ||
| * docked composer; from `md` it is anchored in the gap below the composer frame. A fading lit window | ||
| * of 4x8px segments sweeps across the strip. Renders nothing while the session is idle and has no | ||
| * background activity; otherwise announces the current activity through an indeterminate | ||
| * progressbar. | ||
| */ | ||
| export function ComposerActivityBar({ sessionID }: ComposerActivityBarProps) { | ||
| const status = useSessionStatusForSession(sessionID) | ||
| const hasBackgroundActivity = useHasBackgroundActivity(sessionID) | ||
|
|
||
| if (status.type === 'idle' && !hasBackgroundActivity) { | ||
| return null | ||
| } | ||
|
|
||
| const activity = status.type === 'idle' | ||
| ? ACTIVITY_PRESENTATION.background | ||
| : ACTIVITY_PRESENTATION[status.type] | ||
| const label = status.type === 'retry' | ||
| ? `${ACTIVITY_PRESENTATION.retry.label} (Retry #${status.attempt})` | ||
| : activity.label | ||
|
|
||
| return ( | ||
| <div | ||
| role="progressbar" | ||
| aria-label={label} | ||
| aria-valuemin={0} | ||
| aria-valuemax={100} | ||
| className="pointer-events-none absolute inset-x-3 bottom-4 flex md:-bottom-[11px]" | ||
| > | ||
| <ScannerBars tone={activity.tone} /> | ||
| </div> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.