fix: tidy agents home + mobile logo + PWA dev/icon issues (#1419)

* fix: tidy agents home, mobile logo, and PWA dev/icon issues

- Remove recent-runs cards from the new agent page
- Scale the ASCII logo to fit narrow viewports instead of overflowing
- Serve manifest in dev and skip SW registration in dev to clear console errors
- Use a flat, opaque apple-touch-icon so iOS stops adding a black border
- Ignore generated ui/dev-dist

* feat: add send button and prevent iOS focus-zoom on chat input

- Add a circular send button (accent, spinner while sending) to the prompt bar
- Bump textarea to 16px on mobile so iOS doesn't zoom the viewport on focus

* fix: polish prompt bar and center logo

- Center the ASCII logo at all widths
- Prevent iOS focus-zoom via viewport maximum-scale instead of bumping the
  input to 16px, so mobile text stays the intended size
- Give the model picker a chip/chevron treatment to anchor it next to the send button

* fix: simplify model picker to plain text + chevron

* fix: tighten prompt bar padding and shrink send button

---------

Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-04 16:47:35 -07:00 • committed by GitHub
parent a511add856
commit 6f330f777d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 44 additions and 43 deletions

1
ui/.gitignore vendored
View file

@ -10,5 +10,6 @@ count.txt
.wrangler
.output
.vinxi
dev-dist
__unconfig*
todos.json

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -4,8 +4,11 @@ import { hydrateRoot } from "react-dom/client"
import { registerSW } from "virtual:pwa-register"
// prompt mode: a new SW installs in the background and takes over on the next
// load, so deploys never reload a tab mid-agent-run.
registerSW()
// load, so deploys never reload a tab mid-agent-run. Skip in dev — the SW
// precaches production-only build artifacts that 404 against the dev server.
if (import.meta.env.PROD) {
registerSW()
}
hydrateRoot(
document,

View file

@ -1,19 +1,15 @@
import { Link } from "@tanstack/react-router"
import { useState } from "react"
import type { ModelSelection } from "@/lib/agents/useModelOptions"
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
import { AgentRunCard } from "@/components/agents/AgentRunCard"
import { SlackConnectDialog } from "@/components/agents/SlackConnectDialog"
import { Logo } from "@/components/agents/ported/Logo"
import { useAgentThreads, useCreateAgentThread } from "@/lib/agents/queries"
import { useCreateAgentThread } from "@/lib/agents/queries"
import { useModelOptions } from "@/lib/agents/useModelOptions"
import { useProfile, useRepos } from "@/lib/profile"
export function AgentsHome() {
const threadsQuery = useAgentThreads()
const createThread = useCreateAgentThread()
const recentRuns = (threadsQuery.data ?? []).slice(0, 5)
const { models, defaultSelection } = useModelOptions()
const [selection, setSelection] = useState<ModelSelection | null>(null)
const activeSelection = selection ?? defaultSelection
@ -53,35 +49,7 @@ export function AgentsHome() {
onRepoChange={setRepoOverride}
/>
</div>
<div className="mx-auto mt-6 w-full max-w-[640px] space-y-2">
{threadsQuery.isLoading ? (
<p className="text-center text-sm text-[var(--ui-text-dim)]">
Loading agents…
</p>
) : recentRuns.length === 0 ? (
<AgentsHomeEmptyState />
) : (
recentRuns.map((thread) => (
<AgentRunCard key={thread.id} thread={thread} />
))
)}
</div>
</div>
</div>
)
}
export function AgentsHomeEmptyState() {
return (
<div className="flex flex-col items-center gap-3 py-4 text-center">
<p className="text-sm text-[var(--ui-text-muted)]">No agents yet.</p>
<Link
to="/agents"
className="text-sm font-medium text-[var(--ui-accent)] hover:underline"
>
Start your first agent
</Link>
</div>
)
}

View file

@ -1,3 +1,4 @@
import { ArrowUp, ChevronDown, LoaderCircle } from "lucide-react"
import {
memo,
useCallback,
@ -121,7 +122,7 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
)}
<div
className={cn(
"relative flex min-h-[106px] flex-col rounded-2xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-4 py-3.5 shadow-sm",
"relative flex min-h-[106px] flex-col rounded-2xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-3 py-2.5 shadow-sm",
compact && "min-h-[88px]"
)}
>
@ -134,7 +135,7 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
placeholder={busy ? "Send a message to queue next..." : placeholder}
disabled={disabled}
className={cn(
"w-full min-w-0 resize-none overflow-hidden bg-transparent leading-[1.45] text-[color:var(--ui-text)] outline-none placeholder:text-[color:var(--ui-text-dim)]",
"w-full min-w-0 resize-none overflow-hidden bg-transparent text-[13px] leading-[1.45] text-[color:var(--ui-text)] outline-none placeholder:text-[color:var(--ui-text-dim)]",
compact ? "min-h-[36px]" : "min-h-[52px]"
)}
style={{ maxHeight: PROMPT_TEXTAREA_MAX_HEIGHT }}
@ -146,9 +147,12 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
type="button"
disabled={pickerDisabled}
onClick={() => setModelDropdownOpen((open) => !open)}
className="max-w-[220px] cursor-pointer truncate text-[color:var(--ui-text-muted)] transition-opacity hover:opacity-80 disabled:cursor-default disabled:opacity-60"
className="flex max-w-[220px] cursor-pointer items-center gap-0.5 text-[13px] text-[color:var(--ui-text-muted)] transition-opacity hover:opacity-80 disabled:cursor-default disabled:opacity-60"
>
{selectionLabel}
<span className="truncate">{selectionLabel}</span>
{!pickerDisabled && (
<ChevronDown className="size-3.5 shrink-0 opacity-60" />
)}
</button>
{modelDropdownOpen && combos.length > 0 && (
<div className="absolute bottom-full left-0 z-50 mb-1 max-h-72 overflow-hidden overflow-y-auto rounded border border-[var(--ui-border)] bg-[var(--ui-surface)] shadow-lg">
@ -184,6 +188,20 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
</div>
)}
</div>
<button
type="button"
onClick={handleSubmit}
disabled={!value.trim() || disabled}
aria-label="Send message"
className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-full bg-[var(--ui-accent)] text-white transition-opacity hover:opacity-90 disabled:cursor-default disabled:opacity-40"
>
{disabled ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<ArrowUp className="size-3.5" strokeWidth={2.5} />
)}
</button>
</div>
</div>
</div>

View file

@ -12,8 +12,13 @@ export function Logo() {
].join("\n");
return (
<pre className="m-0 max-w-full overflow-x-auto font-mono text-[7px] leading-none text-[#87CEEB]">
{ascii}
</pre>
<div className="w-full" style={{ containerType: "inline-size" }}>
<pre
className="m-0 w-full overflow-hidden text-center font-mono leading-none text-[#87CEEB]"
style={{ fontSize: "min(7px, 1.9cqw)" }}
>
{ascii}
</pre>
</div>
);
}

View file

@ -17,7 +17,10 @@ export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
name: "viewport",
content: "width=device-width, initial-scale=1, maximum-scale=1",
},
{ name: "theme-color", content: "#000000" },
{ title: "open-swe" },
],

View file

@ -20,6 +20,9 @@ const config = defineConfig({
injectRegister: false,
registerType: "prompt",
outDir: ".output/public",
devOptions: {
enabled: true,
},
integration: {
closeBundleOrder: "pre",
},