Skip to content

Commit e4434b2

Browse files
authored
improvement(mobile): refine responsive chat and workspace layouts (#8842)
Make navigation, chat composers, resource controls, settings, and workflow panels fit narrow touch screens while preserving desktop and fullscreen layouts. Add the Chromium/WebKit mobile CI matrix and the mobile design-review checklist. Keep workflow actions visible in compact panels and attachment scrolling bounded, with shared cues for hidden content. Stabilize clock-sensitive billing assertions and the post-commit connector deletion test without changing production behavior.
1 parent 2ab3211 commit e4434b2

155 files changed

Lines changed: 3828 additions & 1519 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/emcn-design-review/SKILL.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ Use `ChipConfirmModal` (title "Delete/Remove {ItemType}", `confirm={{ label, onC
6262

6363
Default: `size-[14px]`. Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 12px > 20px. Use the `size-*` shorthand — flag `h-[Npx] w-[Npx]` and `h-N w-N` pairs as refactor targets.
6464

65+
## Mobile
66+
67+
Check at 320px and 390px with touch, then desktop and fullscreen. Use `Chip`/`ChipLink`'s `mobileIconOnly` for familiar mobile toolbar actions, preserving accessible names; keep labels for ambiguous choices. Keep navigation, primary actions, and dismissal reachable without hover or dragging; aim for 44px touch targets with compact visible icons/button faces and 16px editable text. Match behavior to the actual container or viewport breakpoint. Contain horizontal scrolling to intentional tables/code, fit overlays to the dynamic viewport, and keep the composer/actions reachable with the keyboard open. Reuse EMCN tokens and brief motion with reduced-motion support; preserve desktop geometry.
68+
6569
## Anti-patterns to flag
6670

6771
- Raw `<button>`/`<input>`, or legacy `Input`/`Textarea`/`Modal`, instead of the canonical chip components (`ChipInput`/`ChipTextarea`/`ChipModal`)

‎.claude/rules/sim-testing.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ contracts, and demonstrated regressions.
4747
(`sim_test`); `TEST_REDIS_URL` must be loopback. `packages/db/testing/test-infrastructure.ts` owns
4848
those checks. Isolate with a unique schema or generated IDs, and clean up in `afterAll`.
4949
- Integration files run one at a time against one shared database. A new `*.integration.ts` is
50-
picked up by CI with no workflow change, and the run writes `test-results/integration.json`, which
51-
CI uploads. Never add a passing suite to the quarantine list in `apps/sim/vitest.config.ts`.
50+
picked up by CI with no workflow change, and the run writes the JSON report configured in
51+
`vitest.shared.ts`, which CI uploads. Never add a passing suite to the quarantine list in
52+
`apps/sim/vitest.config.ts`.
5253
- `bun run test:integration` starts disposable Postgres and Redis containers, provisions the schema,
5354
and runs both workspaces; pass filenames to narrow the `apps/sim` run.
5455

‎.cursor/rules/sim-testing.mdc‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ contracts, and demonstrated regressions.
4545
(`sim_test`); `TEST_REDIS_URL` must be loopback. `packages/db/testing/test-infrastructure.ts` owns
4646
those checks. Isolate with a unique schema or generated IDs, and clean up in `afterAll`.
4747
- Integration files run one at a time against one shared database. A new `*.integration.ts` is
48-
picked up by CI with no workflow change, and the run writes `test-results/integration.json`, which
49-
CI uploads. Never add a passing suite to the quarantine list in `apps/sim/vitest.config.ts`.
48+
picked up by CI with no workflow change, and the run writes the JSON report configured in
49+
`vitest.shared.ts`, which CI uploads. Never add a passing suite to the quarantine list in
50+
`apps/sim/vitest.config.ts`.
5051
- `bun run test:integration` starts disposable Postgres and Redis containers, provisions the schema,
5152
and runs both workspaces; pass filenames to narrow the `apps/sim` run.
5253

‎.github/scripts/http-e2e.sh‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bash
22
# Runs one end-to-end suite group over real HTTP, each against its own `next dev` app.
33
#
4-
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox> (run from apps/sim)
4+
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|mobile> (run from apps/sim)
55
#
66
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
77
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
@@ -17,7 +17,7 @@
1717
# telemetry flush runs detached and still writes .next/dev).
1818
set -euo pipefail
1919

20-
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox>}
20+
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|mobile>}
2121
report_dir="$RUNNER_TEMP/e2e"
2222
ready_timeout_seconds=300
2323
mkdir -p "$report_dir"
@@ -155,6 +155,19 @@ case "$group" in
155155
bun run test:desktop-inbox:e2e
156156
;;
157157

158+
mobile)
159+
export NEXT_PUBLIC_FORCE_HOSTED=false
160+
export INTERNAL_API_SECRET=mobile-http-ci-local-secret-at-least-32-characters
161+
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=12288"
162+
bunx --no-install playwright install --with-deps chromium webkit
163+
start_app mobile 3024 'mobile browser' record-http-status
164+
MOBILE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
165+
MOBILE_E2E_DATABASE_URL="$DATABASE_URL" \
166+
MOBILE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
167+
MOBILE_E2E_REPORT_PATH="$report_dir/mobile-e2e-report.json" \
168+
bun run test:mobile:e2e
169+
;;
170+
158171
*)
159172
echo "::error::Unknown end-to-end group: $group" >&2
160173
exit 2

‎.github/workflows/checks.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ jobs:
138138
# The suites exercise HTTP behavior rather than a provisioning path, so they run once, against
139139
# the production (migrate) path. Each group's app environment lives in http-e2e.sh.
140140
#
141-
# SCIM runs two suites against a hosted app and is the longest group, so it keeps the 8 vCPU
142-
# runner; the others boot a smaller self-hosted app and fit on 4.
141+
# SCIM's hosted suites and the mobile browser matrix use 8 vCPU; the other
142+
# self-hosted suites fit on 4.
143143
e2e:
144144
name: e2e (${{ matrix.group }})
145145
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && matrix.runner || 'ubuntu-latest' }}
146-
timeout-minutes: 20
146+
timeout-minutes: ${{ matrix.group == 'mobile' && 40 || 20 }}
147147
strategy:
148148
fail-fast: false
149149
matrix:
@@ -156,6 +156,8 @@ jobs:
156156
runner: blacksmith-4vcpu-ubuntu-2404
157157
- group: desktop-inbox
158158
runner: blacksmith-4vcpu-ubuntu-2404
159+
- group: mobile
160+
runner: blacksmith-8vcpu-ubuntu-2404
159161
services:
160162
postgres:
161163
image: pgvector/pgvector:pg17

‎apps/desktop/src/renderer/credential-picker/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function CredentialPicker({ configuration, api }: CredentialPickerProps) {
5757
align='start'
5858
sideOffset={0}
5959
avoidCollisions={false}
60-
className='w-[320px] max-w-none'
60+
className='w-[320px] max-w-none max-md:max-w-none!'
6161
aria-label='Saved passwords'
6262
onOpenAutoFocus={(event) => {
6363
event.preventDefault()

‎apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

3-
import { type RefObject, useCallback, useMemo, useRef, useState } from 'react'
3+
import { type ReactNode, type RefObject, useCallback, useMemo, useRef, useState } from 'react'
4+
import { usePrefersReducedMotion } from '@sim/emcn'
45
import { createLogger } from '@sim/logger'
56
import { generateId } from '@sim/utils/id'
67
import {
@@ -24,6 +25,7 @@ import { useChatStreaming } from '@/app/(interfaces)/chat/hooks'
2425
import SSOAuth from '@/ee/sso/components/sso-auth'
2526
import { useDeployedChatConfig } from '@/hooks/queries/chats'
2627
import { useGitHubStars } from '@/hooks/queries/github-stars'
28+
import { useMobileViewport } from '@/hooks/use-mobile-viewport'
2729

2830
const logger = createLogger('ChatClient')
2931

@@ -51,7 +53,25 @@ function fileToBase64(file: File): Promise<string> {
5153
})
5254
}
5355

56+
interface ChatViewportProps {
57+
children: ReactNode
58+
}
59+
60+
function ChatViewport({ children }: ChatViewportProps) {
61+
const ref = useRef<HTMLDivElement>(null)
62+
useMobileViewport({ ref })
63+
return (
64+
<div
65+
ref={ref}
66+
className='light desktop-title-bar-page fixed inset-0 z-[var(--z-dropdown)] flex flex-col bg-[var(--bg)] text-[var(--text-primary)] max-md:top-[var(--mobile-viewport-top,0px)] max-md:bottom-auto max-md:h-[var(--mobile-viewport-height,100dvh)] max-md:min-h-0!'
67+
>
68+
{children}
69+
</div>
70+
)
71+
}
72+
5473
export default function ChatClient({ identifier }: { identifier: string }) {
74+
const prefersReducedMotion = usePrefersReducedMotion()
5575
const [messages, setMessages] = useState<ChatMessage[]>([])
5676
const [isLoading, setIsLoading] = useState(false)
5777
const messagesEndRef = useRef<HTMLDivElement>(null)
@@ -95,7 +115,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
95115
* With `force` (jump button), re-pins to bottom.
96116
*/
97117
const scrollToBottom = (options?: { behavior?: ScrollBehavior; force?: boolean }) => {
98-
const behavior = options?.behavior ?? 'smooth'
118+
const behavior = prefersReducedMotion ? 'auto' : (options?.behavior ?? 'smooth')
99119
const force = options?.force === true
100120
if (!force && !stickToBottomRef.current) return
101121
if (!messagesEndRef.current) return
@@ -125,7 +145,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
125145

126146
container.scrollTo({
127147
top: container.scrollTop + messageRect.top - containerRect.top,
128-
behavior: 'smooth',
148+
behavior: prefersReducedMotion ? 'auto' : 'smooth',
129149
})
130150
}
131151

@@ -309,7 +329,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
309329
}
310330

311331
return (
312-
<div className='light desktop-title-bar-page fixed inset-0 z-[var(--z-dropdown)] flex flex-col bg-[var(--bg)] text-[var(--text-primary)]'>
332+
<ChatViewport>
313333
<DesktopTitleBarLane />
314334
<ChatHeader chatConfig={chatConfig} starCount={starCount} />
315335

@@ -323,7 +343,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
323343
chatConfig={chatConfig}
324344
/>
325345

326-
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
346+
<div className='relative shrink-0 md:p-4 md:pb-6'>
327347
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
328348
<ChatInput
329349
onSubmit={(value, files) => {
@@ -334,6 +354,6 @@ export default function ChatClient({ identifier }: { identifier: string }) {
334354
/>
335355
</div>
336356
</div>
337-
</div>
357+
</ChatViewport>
338358
)
339359
}

‎apps/sim/app/(interfaces)/chat/components/header/header.tsx‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { SimWordmark } from '@sim/emcn'
3+
import { OverflowText, SimWordmark } from '@sim/emcn'
44
import Image from 'next/image'
55
import Link from 'next/link'
66
import { GithubIcon } from '@/components/icons'
@@ -27,33 +27,35 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
2727
return (
2828
<nav
2929
aria-label='Chat navigation'
30-
className='flex w-full items-center justify-between px-4 pt-3 pb-[21px] sm:px-8 sm:pt-[8.5px] md:px-[44px] md:pt-4'
30+
className='flex w-full shrink-0 items-center justify-between gap-3 px-4 pt-3 pb-[21px] sm:px-8 sm:pt-[8.5px] md:px-[44px] md:pt-4'
3131
>
32-
<div className='flex items-center gap-[34px]'>
33-
<div className='flex items-center gap-3'>
32+
<div className='min-w-0 flex-1'>
33+
<div className='flex min-w-0 items-center gap-3'>
3434
{customImage && (
3535
<Image
3636
src={customImage}
3737
alt={`${chatConfig?.title || 'Chat'} logo`}
3838
width={24}
3939
height={24}
4040
unoptimized
41-
className='size-6 rounded-md object-cover'
41+
className='size-6 shrink-0 rounded-md object-cover'
4242
/>
4343
)}
44-
<h2 className='text-[var(--text-primary)] text-lg'>
45-
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
44+
<h2 className='min-w-0 text-[var(--text-primary)] text-lg'>
45+
<OverflowText
46+
label={chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
47+
/>
4648
</h2>
4749
</div>
4850
</div>
4951

5052
{!brand.logoUrl && (
51-
<div className='flex items-center gap-4'>
53+
<div className='flex shrink-0 items-center gap-4'>
5254
<a
5355
href='https://github.com/simstudioai/sim'
5456
target='_blank'
5557
rel='noopener noreferrer'
56-
className='flex items-center gap-2 text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-primary)]'
58+
className='hidden items-center gap-2 text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-primary)] md:flex'
5759
aria-label={`GitHub repository - ${starCount} stars`}
5860
>
5961
<GithubIcon className='size-[16px]' aria-hidden='true' />

0 commit comments

Comments
 (0)