Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,6 @@
"start": {
"title": "Start your agent",
"connected": "Connected",
"connectedStatus": "✓ Connected",
"waiting": "Waiting for your agent to connect…"
},
"hello": {
Expand Down
1 change: 0 additions & 1 deletion frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,6 @@
"start": {
"title": "启动智能体",
"connected": "已连接",
"connectedStatus": "✓ 已连接",
"waiting": "正在等待智能体连接…"
},
"hello": {
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/v2/__tests__/V2FirstRunHero.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,18 @@ describe('V2FirstRunHero', () => {
expect(screen.getByRole('dialog', { name: 'Bring your agent into your pod' }))
.toHaveAttribute('aria-modal', 'true');
expect(screen.getByText('Waiting for your agent to connect…')).toBeInTheDocument();
// The last step is drawn while waiting, but cannot be used yet.
expect(screen.getByRole('button', { name: 'Say hello' })).toBeDisabled();
expect(screen.getByRole('link', { name: /Open connection setup/i })).toHaveAttribute('target', '_blank');

await act(async () => {
jest.advanceTimersByTime(3_000);
await Promise.resolve();
});

expect(await screen.findByText('✓ Connected')).toBeInTheDocument();
// Connected is a check glyph and the word, under the step's own title.
expect(await screen.findByText('Connected')).toHaveClass('v2-first-run__connected');
expect(screen.getByText('Start your agent')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Say hello' }));
await flush();

Expand Down Expand Up @@ -348,10 +352,13 @@ describe('V2FirstRunHero', () => {
.toBeInTheDocument();
expect(localStorage.getItem(FIRST_RUN_STARTED_KEY)).toBe('1');
await flush();
expect(await screen.findByRole(
const control = await screen.findByRole(
status.connected ? 'button' : 'link',
{ name: readyControl },
)).toBeInTheDocument();
);
// Say hello is drawn from the start, so only an enabled one means connected.
if (status.connected) await waitFor(() => expect(control).toBeEnabled());
else expect(screen.getByRole('button', { name: 'Say hello' })).toBeDisabled();

fireEvent.click(screen.getByRole('button', { name: 'Skip for now' }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
Expand All @@ -364,7 +371,8 @@ describe('V2FirstRunHero', () => {
renderHero();
await flush();

expect(await screen.findByRole('button', { name: 'Say hello' })).toBeInTheDocument();
const hello = await screen.findByRole('button', { name: 'Say hello' });
await waitFor(() => expect(hello).toBeEnabled());
});

test('a Guide request made before the hero mounts opens it on the shell', async () => {
Expand Down
28 changes: 19 additions & 9 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1374,16 +1374,25 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(ruleBody(v2, '.v2-first-run')).toContain('overflow-y: auto');
});

test('the first-run setup CTA beats the global inherited anchor color with ink', () => {
test('the first-run setup CTA beats the global inherited anchor color with its own', () => {
// `.v2-root a { color: inherit }` outranks a bare class selector. The
// first browser pass rendered inherited text on the filled CTA until this
// compound selector matched/exceeded the reset specificity.
const rule = ruleBody(
v2,
'.v2-root a.v2-first-run__setup,\n.v2-root button.v2-first-run__hello',
);
expect(rule).toContain('background: var(--v2-ink)');
expect(rule).toContain('color: var(--v2-on-ink)');
// The shared key rule lists the anchor on its second line, so read its own, later block.
expect(lastRuleBody(v2, '.v2-root a.v2-first-run__setup')).toContain('color: var(--v2-text-primary)');
expect(lastRuleBody(v2, '.v2-root a.v2-first-run__setup')).toContain('linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b))');
expect(ruleBody(v2, '.v2-root button.v2-first-run__hello')).toContain('color: var(--v2-on-fill)');
});

test('the first-run dialog is a bottom sheet on a phone, its waiting dot still under reduced motion', () => {
// D215 phone: the dialog sits on the bottom edge with 44px touch targets.
const phone = mediaBlockContaining(v2, '.v2-first-run__overlay {');
expect(ruleBody(v2, '.v2-first-run__overlay', { within: phone })).toContain('place-items: end center');
expect(ruleBody(v2, '.v2-root button.v2-first-run__skip', { within: phone })).toContain('height: 44px');
expect(phone).toMatch(/\.v2-root a\.v2-first-run__setup,\n\s+\.v2-root button\.v2-first-run__hello \{\s+height: 44px;/);
expect(ruleBody(v2, '.v2-first-run__title')).toContain('text-wrap: balance');
expect(ruleBody(v2, '.v2-first-run__dot::after')).toContain('animation: v2-presence-pulse');
expect(v2).toMatch(/@media \(prefers-reduced-motion: reduce\) \{\s+\.v2-first-run__dot::after \{ animation: none; \}/);
});

test('Settings is the one home for the folded self-profile and device routes', () => {
Expand Down Expand Up @@ -2270,15 +2279,16 @@ describe('v2 layout invariants (CSS rule presence)', () => {
describe('Signal chat + inspector color contract (TASK-124)', () => {
// The composer's send and mode buttons left with the old composer (TASK-758).
const filledControls = [
'.v2-root button.v2-first-run__hello',
// The first-run hello takes the Signal fill, the ink gradient (D215 keys).
'\n.v2-root button.v2-first-run__hello {',
'.v2-syscard__cta',
'.v2-root button.v2-approval__btn--approve',
];

test('every ordinary filled control uses ink, never cobalt', () => {
for (const selector of filledControls) {
const rule = selectorRuleBody(v2, selector);
expect(rule).toContain('var(--v2-ink)');
expect(rule).toMatch(/var\(--v2-(?:ink|fill-a)\)/);
expect(rule).not.toContain('var(--v2-accent)');
}
});
Expand Down
37 changes: 20 additions & 17 deletions frontend/src/v2/components/V2FirstRunHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { useNavigate } from 'react-router-dom';
import { useV2Api } from '../hooks/useV2Api';
import { useTranslation } from 'react-i18next';
import { FIRST_RUN_REOPEN_EVENT, FIRST_RUN_STARTED_KEY } from '../firstRunGuide';
import { ShellGlyph } from '../icons/glyphs';

const POLL_INTERVAL_MS = 3_000;
const CHECK_MARK = '✓';
const EXTERNAL_LINK_MARK = '↗';
export { FIRST_RUN_STARTED_KEY } from '../firstRunGuide';

Expand Down Expand Up @@ -48,7 +48,7 @@ const writeFlag = (key: string, value: boolean): void => {

const StepMark: React.FC<{ done: boolean; children: React.ReactNode }> = ({ done, children }) => (
<span className={`v2-first-run__step-mark${done ? ' v2-first-run__step-mark--done' : ''}`} aria-hidden="true">
{done ? CHECK_MARK : children}
{done ? <ShellGlyph name="check" /> : children}
</span>
);

Expand Down Expand Up @@ -281,11 +281,13 @@ const V2FirstRunHero: React.FC<V2FirstRunHeroProps> = ({ onVisibilityChange }) =
<li className="v2-first-run__step">
<StepMark done={Boolean(status?.connected)}>2</StepMark>
<div className="v2-first-run__step-body">
<strong>{status?.connected ? t('firstRun.steps.start.connected') : t('firstRun.steps.start.title')}</strong>
<span className={status?.connected ? 'v2-first-run__connected' : ''} role="status" aria-live="polite">
{status?.connected ? t('firstRun.steps.start.connectedStatus') : t('firstRun.steps.start.waiting')}
<strong>{t('firstRun.steps.start.title')}</strong>
<span className={status?.connected ? 'v2-first-run__connected' : 'v2-first-run__waiting'} role="status" aria-live="polite">
{status?.connected
? <><ShellGlyph name="check" />{t('firstRun.steps.start.connected')}</>
: <><span className="v2-first-run__dot" aria-hidden="true" />{t('firstRun.steps.start.waiting')}</>}
</span>
{statusError && <span className="v2-first-run__error">{statusError}</span>}
{statusError && <span className="v2-first-run__error"><ShellGlyph name="alert" />{statusError}</span>}
</div>
</li>

Expand All @@ -294,17 +296,18 @@ const V2FirstRunHero: React.FC<V2FirstRunHeroProps> = ({ onVisibilityChange }) =
<div className="v2-first-run__step-body">
<strong>{t('firstRun.steps.hello.title')}</strong>
<span>{t('firstRun.steps.hello.text')}</span>
{status?.connected && status.connectedAgent && (
<button
type="button"
className="v2-first-run__hello"
onClick={sayHello}
disabled={openingRoom}
>
{openingRoom ? t('firstRun.steps.hello.opening') : t('firstRun.steps.hello.cta')}
</button>
)}
{roomError && <span className="v2-first-run__error">{roomError}</span>}
{/* Shown before the agent connects, disabled, so the last step
reads as one click away (D215 waiting frame). */}
<button
type="button"
className="v2-first-run__hello"
onClick={sayHello}
disabled={openingRoom || !(status?.connected && status.connectedAgent)}
aria-busy={openingRoom || undefined}
>
{openingRoom ? t('firstRun.steps.hello.opening') : t('firstRun.steps.hello.cta')}
</button>
{roomError && <span className="v2-first-run__error" role="alert"><ShellGlyph name="alert" />{roomError}</span>}
</div>
</li>
</ol>
Expand Down
Loading
Loading