[feat] Rebuild mobile Home as one composer over one list - #6732
[feat] Rebuild mobile Home as one composer over one list#6732ashrafchowdury wants to merge 39 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 SummarySummary by CodeRabbit
WalkthroughThe PR replaces the mobile home dashboard with a focused task and agent-creation surface. It adds shared home UI components, handoff actions, loading states, and live dictation waveform support for chat composers. ChangesHome focus experience
Dictation waveform
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant HomeFocus
participant useHomeHandoff
participant ChatRoute
User->>HomeFocus: submit task or create prompt
HomeFocus->>useHomeHandoff: send text and attachments
useHomeHandoff->>ChatRoute: stash task and navigate
ChatRoute-->>useHomeHandoff: navigation result
sequenceDiagram
participant VoiceComposer
participant DictationAnalyser
participant Microphone
participant RichChatInput
VoiceComposer->>DictationAnalyser: activate during dictation
DictationAnalyser->>Microphone: request audio stream
Microphone-->>DictationAnalyser: provide analyser data
DictationAnalyser->>RichChatInput: expose analyser ref
RichChatInput->>RichChatInput: render waveform
Merge Risk: 🟡 Moderate · up to Mobile Home can hide agent loading and failure states, lose template instructions, or mishandle failed task and agent-creation actions. Dictation also has bounded microphone and waveform issues, while several primary controls lack keyboard focus indicators; these issues should be resolved before release. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Railway Preview Environment
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
web/packages/agenta-ui/src/RichChatInput/ComposerSendButton.tsx (1)
44-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the literal hex fallbacks with theme tokens.
--ag-composer-send-fgfalls back to#191a0dand--ag-composer-send-hover-bgfalls back to#b8cb3f. A literal hex value resolves to one colour in both light and dark themes, so any host that sets only--ag-composer-send-bggets a foreground that does not follow the theme. The background already uses a token fallback (--ag-surface-accent); use equivalent tokens for the other two.The coding guidelines state: "All theme-aware colors flow from one source of truth,
oss/src/styles/theme/palette.ts" and "When adding or changing UI elements, implement appearance and interaction states for both light and dark themes".Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 849ea087-0a54-4e73-b13b-9d371c8201e0
📒 Files selected for processing (28)
web/mobile/src/features/agents/AgentComposer.tsxweb/mobile/src/features/agents/useNewAgentAction.tsweb/mobile/src/features/chat/Composer.tsxweb/mobile/src/features/home/HomeComposer.tsxweb/mobile/src/features/home/HomeScreen.tsxweb/mobile/src/features/home/states/HomeSkeleton.tsxweb/mobile/src/features/home/states/HomeStates.tsxweb/mobile/src/features/home/useHomeHandoff.tsweb/mobile/src/features/onboarding/FirstRunComposer.tsxweb/mobile/src/styles/globals.cssweb/packages/agenta-chat/src/components/ChatComposer.tsxweb/packages/agenta-chat/src/components/RecordingWaveform.tsxweb/packages/agenta-chat/src/components/VoiceInputButton.tsxweb/packages/agenta-chat/src/hooks/audioContext.tsweb/packages/agenta-chat/src/hooks/index.tsweb/packages/agenta-chat/src/hooks/useAudioRecorder.tsweb/packages/agenta-chat/src/hooks/useDictationAnalyser.tsweb/packages/agenta-chat/src/hooks/useVoiceComposer.tsweb/packages/agenta-entity-ui/src/agent/AgentPicker.tsxweb/packages/agenta-home-ui/src/HomeEntityList.tsxweb/packages/agenta-home-ui/src/HomeFocus.tsxweb/packages/agenta-home-ui/src/HomeGreeting.tsxweb/packages/agenta-home-ui/src/HomeTaskComposer.tsxweb/packages/agenta-home-ui/src/TemplateProviderMarks.tsxweb/packages/agenta-home-ui/src/index.tsweb/packages/agenta-ui/src/RichChatInput/ComposerSendButton.tsxweb/packages/agenta-ui/src/RichChatInput/RichChatInput.tsxweb/packages/agenta-ui/src/components/presentational/logo-marks/LogoMarks.tsx
💤 Files with no reviewable changes (1)
- web/mobile/src/features/home/HomeComposer.tsx
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.
| <button | ||
| type="button" | ||
| onClick={onRetry} | ||
| className="text-foreground hover:bg-accent rounded-control-sm border-border box-border cursor-pointer appearance-none border border-solid bg-transparent px-2 py-1 font-[inherit] text-xs outline-none" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add a visible focus state to the "Try again" button.
outline-none removes the browser focus ring, and no focus-visible:* classes replace it. A keyboard user then cannot see when this button is focused. Apply the same focus recipe the mobile theme uses elsewhere, for example web/mobile/src/features/onboarding/FirstRunComposer.tsx line 88.
♿ Proposed fix
- className="text-foreground hover:bg-accent rounded-control-sm border-border box-border cursor-pointer appearance-none border border-solid bg-transparent px-2 py-1 font-[inherit] text-xs outline-none"
+ className="text-foreground hover:bg-accent rounded-control-sm border-border focus-visible:border-ring focus-visible:ring-ring/50 box-border cursor-pointer appearance-none border border-solid bg-transparent px-2 py-1 font-[inherit] text-xs outline-none focus-visible:ring-[3px]"Based on learnings: "In web/mobile TypeScript React components using the Tailwind v4 shadcn theme, use the focus-ring classes outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50. Do not add ring-offset classes because this mobile theme does not provide them."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className="text-foreground hover:bg-accent rounded-control-sm border-border box-border cursor-pointer appearance-none border border-solid bg-transparent px-2 py-1 font-[inherit] text-xs outline-none" | |
| className="text-foreground hover:bg-accent rounded-control-sm border-border focus-visible:border-ring focus-visible:ring-ring/50 box-border cursor-pointer appearance-none border border-solid bg-transparent px-2 py-1 font-[inherit] text-xs outline-none focus-visible:ring-[3px]" |
Source: Learnings
There was a problem hiding this comment.
Fixed. Applied the mobile theme's focus recipe (focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50, no ring-offset).
| try { | ||
| await router.push(`${base}/sessions/${sessionId}?agent=${agentId}`) | ||
| } catch (error) { | ||
| // The chat route never mounted, so drop the stash — otherwise the task replays the | ||
| // next time this session id is opened. Attachments stay staged, still sendable. | ||
| dropPendingTask(sessionId) | ||
| console.error("[useHomeHandoff] could not open the session", error) | ||
| return | ||
| } | ||
| // Cleared only once the destination is committed to. | ||
| attachments.clearAttachments(staged.map((file) => file.uid)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle the Pages Router’s resolved false result before clearing attachments.
web/mobile uses Next.js 16.3.0 with useRouter from next/router. A cancelled transition resolves false; it does not enter catch. The code then clears attachments while the task remains in pendingTasksAtom. Drop the task when navigation returns false, and retain the rejection cleanup path.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await router.push(`${base}/sessions/${sessionId}?agent=${agentId}`) | |
| } catch (error) { | |
| // The chat route never mounted, so drop the stash — otherwise the task replays the | |
| // next time this session id is opened. Attachments stay staged, still sendable. | |
| dropPendingTask(sessionId) | |
| console.error("[useHomeHandoff] could not open the session", error) | |
| return | |
| } | |
| // Cleared only once the destination is committed to. | |
| attachments.clearAttachments(staged.map((file) => file.uid)) | |
| const navigated = await router | |
| .push(`${base}/sessions/${sessionId}?agent=${agentId}`) | |
| .catch(() => false) | |
| if (!navigated) { | |
| // The chat route never mounted, so drop the stash — otherwise the task replays the | |
| // next time this session id is opened. Attachments stay staged, still sendable. | |
| dropPendingTask(sessionId) | |
| return | |
| } | |
| // Cleared only once the destination is committed to. | |
| attachments.clearAttachments(staged.map((file) => file.uid)) |
There was a problem hiding this comment.
Fixed, and thank you — useNewAgentAction documents this exact hazard and I did not carry it across. Catching alone cleared the attachments while the task sat unplayed in the stash. Now .catch(() => false) and both outcomes go through the !navigated branch.
| const barWidth = Math.max(1, Math.min(BAR_WIDTH, width / HISTORY)) | ||
| // The remainder, so the strip still spans the full width at whatever size it is given. | ||
| const gap = HISTORY > 1 ? Math.max(1, (width - barWidth * HISTORY) / (HISTORY - 1)) : 0 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent waveform clipping below 223px. With HISTORY = 64, the current 2.5px bars and 1px gaps require 223px. At smaller widths, the final bar can extend past canvas.clientWidth. Remove both Math.max(1, ...) minimums and clamp the gap to zero so the bars fit narrow canvases.
There was a problem hiding this comment.
Fixed. Both Math.max(1, ...) floors are gone: the bar takes its share of a narrow strip and the gap closes to zero. Checked the last bar's right edge at 60/100/160/223/300/500px; it fits at every one.
| } | ||
| streamRef.current = stream | ||
| const Ctor = getAudioContextCtor() | ||
| if (!Ctor) return |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Release failed analyser streams. Tear down the stream and context here, or the auxiliary microphone track remains active until dictation ends.
Also applies to: 63-65
There was a problem hiding this comment.
Fixed. The !Ctor path now tears down immediately. There is nothing to measure without an AudioContext, so holding the microphone for the length of the dictation was pure cost.
| const focusSoon = useCallback((seed?: string) => { | ||
| requestAnimationFrame(() => { | ||
| if (seed !== undefined) inputRef.current?.setMarkdown(seed) | ||
| inputRef.current?.focus() | ||
| }) | ||
| }, []) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve the template seed until RichChatInput mounts.
ChatComposer lazy-loads RichChatInput behind Suspense. When template selection occurs before that import resolves, focusSoon runs with a null inputRef, drops templateBuilderMessage(template), and never retries. Create mode then submits empty text with only templateName. Store the pending seed and apply it when the handle becomes available. Do not rely only on initialMarkdown, because RichChatInput reads that prop only at mount.
There was a problem hiding this comment.
Fixed, and this was the real one. It also explains focus landing intermittently in my own testing. The seed now retries per frame until the handle exists, bounded at 120 frames so a composer that never mounts cannot spin.
| onSubmit={async (text) => { | ||
| if (creating) { | ||
| await onCreate?.({text}) | ||
| return | ||
| } | ||
| if (!effectiveAgentId) return | ||
| await onStart({agentId: effectiveAgentId, text}) | ||
| }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
# Check whether ChatComposer awaits or catches the onSubmit promise.
fd -t f 'ChatComposer.tsx' | xargs -r rg -n -C6 'onSubmit'
fd -t f 'RichChatInput.tsx' | xargs -r rg -n -C6 'onSubmit|catch'Repository: Agenta-AI/agenta
Length of output: 6912
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- HomeTaskComposer ---'
fd -t f 'HomeTaskComposer.tsx' | xargs -r sed -n '190,255p'
printf '%s\n' '--- HomeFocus bindings ---'
fd -t f 'HomeFocus.tsx' | xargs -r rg -n -C12 'onCreateFromPrompt|onStartTask|HomeTaskComposer|onSubmitError'
printf '%s\n' '--- error-related composer props and call sites ---'
fd -t f -e ts -e tsx . | xargs -r rg -n -C3 'onSubmitError|onCreate\??:|onStart\??:|onCreateFromPrompt|onStartTask'Repository: Agenta-AI/agenta
Length of output: 34372
🤖 get_repo_knowledge executed:
get_repo_knowledge Agenta-AI/agenta /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/learnings /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/conventions
Length of output: 8933
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- HomeTaskComposer submit handler and props ---'
fd -t f 'HomeTaskComposer.tsx' | xargs -r sed -n '1,75p;220,245p'
printf '%s\n' '--- RichChatInput submit implementation ---'
fd -t f 'RichChatInput.tsx' | xargs -r sed -n '168,235p'
printf '%s\n' '--- mobile handoff implementations ---'
fd -t f 'useHomeHandoff.ts' | xargs -r sed -n '1,95p'
printf '%s\n' '--- HomeFocus consumers ---'
rg -n -C8 'onStartTask=|onCreateFromPrompt=|useHomeHandoff|errorSlot' web/mobile web/packages/agenta-home-uiRepository: Agenta-AI/agenta
Length of output: 32109
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- reviewed file diff ---'
git diff --unified=20 -- web/packages/agenta-home-ui/src/HomeTaskComposer.tsxRepository: Agenta-AI/agenta
Length of output: 181
Handle rejected submit callbacks in HomeTaskComposer.
RichChatInput invokes onSubmit without awaiting or catching its promise. The create path awaits newAgent.createFromPrompt without a catch, so a creation failure can become an unhandled rejection with no user feedback. Add an error callback or visible error state, and catch the rejection in HomeTaskComposer.
There was a problem hiding this comment.
Fixed. My own review pass deleted onStartError as dead API and took the try/catch with it. Both hosts already report their own failures, so this catches and logs rather than reintroducing the callback.
a222605 to
fc965a2
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
web/packages/agenta-home-ui/src/HomeFocus.tsx (1)
11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueComment blocks exceed the one-line limit.
The coding guidelines set a hard rule of at most one short line per comment. The doc blocks here and at lines 47-56, 71-75, and 117-119 run to several lines, and the same pattern appears in
HomeEntityList.tsx. Condense each to one line, or move the longer rationale into the package README.As per coding guidelines: "Hard rule. At most ONE short line per comment."
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Advanced
Run ID: 9ee09c3f-cea3-420e-bfcd-85c147ce6104
📒 Files selected for processing (10)
web/mobile/src/features/home/HomeScreen.tsxweb/mobile/src/features/home/homeSurface.tsweb/mobile/src/features/onboarding/FirstRunComposer.tsxweb/mobile/src/features/onboarding/FirstRunScreen.tsxweb/mobile/src/features/onboarding/copy.tsweb/mobile/src/features/onboarding/homeSurface.tsweb/mobile/src/features/onboarding/states/FirstRunStates.tsxweb/mobile/tests/unit/homeSurface.test.tsweb/packages/agenta-home-ui/src/HomeEntityList.tsxweb/packages/agenta-home-ui/src/HomeFocus.tsx
💤 Files with no reviewable changes (5)
- web/mobile/src/features/onboarding/FirstRunComposer.tsx
- web/mobile/src/features/onboarding/copy.ts
- web/mobile/src/features/onboarding/FirstRunScreen.tsx
- web/mobile/src/features/onboarding/homeSurface.ts
- web/mobile/src/features/onboarding/states/FirstRunStates.tsx
Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.
| const ROW = | ||
| "box-border flex w-full cursor-pointer appearance-none items-center gap-3.5 rounded-[10px] border-0 bg-transparent px-3.5 py-2 text-left font-[inherit] outline-none transition-colors hover:bg-accent" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a visible focus style with outline-none.
ROW, TAB_BASE, and the "New agent" button all set outline-none and define no replacement. Keyboard users get no focus indicator on the page's main controls.
♿ Proposed fix
const ROW =
- "box-border flex w-full cursor-pointer appearance-none items-center gap-3.5 rounded-[10px] border-0 bg-transparent px-3.5 py-2 text-left font-[inherit] outline-none transition-colors hover:bg-accent"
+ "box-border flex w-full cursor-pointer appearance-none items-center gap-3.5 rounded-[10px] border-0 bg-transparent px-3.5 py-2 text-left font-[inherit] outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"There was a problem hiding this comment.
Fixed. ROW, TAB_BASE and the New agent button share a FOCUS_RING constant using the theme's recipe.
| const hasAgentsTab = agents.length > 0 | ||
| const showAgents = hasAgentsTab && tab === "agents" | ||
| const agentsBody = errorSlot ?? (loading ? loadingSlot : agents.length === 0 ? emptySlot : null) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
loadingSlot, emptySlot, and errorSlot can never render. Both gates treat "zero agents" as "the project has no agents", but the host supplies these slots exactly when the list is unresolved, empty, or failed. A failed agents fetch therefore shows the templates list with no message and no retry, even though resolveHomeSurface returns "home" so the user can retry.
web/packages/agenta-home-ui/src/HomeEntityList.tsx#L152-L154: include a presentagentsBodyinhasAgentsTab, so the agents tab exists while a state slot is supplied.web/packages/agenta-home-ui/src/HomeFocus.tsx#L79-L81: defaulttabto"agents"whenerrorSlotorloadingis set, so the selected tab is the one that owns the state slot.
📍 Affects 2 files
web/packages/agenta-home-ui/src/HomeEntityList.tsx#L152-L154(this comment)web/packages/agenta-home-ui/src/HomeFocus.tsx#L79-L81
There was a problem hiding this comment.
Fixed, and this was a regression I introduced when I hid the agents tab for an empty roster. hasAgentsTab is now agents.length > 0 || agentsBody !== null, and the tab defaults to agents while loading or on error, so a failed fetch shows its retry instead of silently landing on templates.
46f5ed2 to
8ec8c56
Compare
fc965a2 to
4362add
Compare
8ec8c56 to
e90abf2
Compare
Home carried six competing headers: a hero and composer, then Sessions and Automation runs beside a rail of agents, next triggers and usage. None of it was the page's own job — every panel summarised another page. Mobile's Home is now a centred column that keeps one job and one list: a greeting, the question, the composer, and a single region switched between the agents you have and the templates you could start from, so the two never compete for the page. "+ New" flips the same composer into create mode rather than opening a second surface. The shape lives in @agenta/home-ui beside HomeOverview, which desktop still renders unchanged; web/mobile supplies only its routing verbs and its designed states. HomeTaskComposer picks up the create mode and drops its local select for the app's one AgentPicker.
The create-mode ring took a 13px radius while the composer inside it takes 8px, so the corners showed a wedge of the sweep rather than a rim; both now follow the composer's own radius. Template tiles used the template colour as text over a 14% tint. That palette is the white-safe deep steps — chosen to carry white monograms — so on a dark surface the initials all but vanished. They get the solid fill the strip cards and the New agent menu already draw.
`triggerClassName` joined onto the built class string, so an override only won if the stylesheet happened to order it last. Home's composer wants a chip with no fill inside an already-filled box; it got one by luck. Merging with `cn` makes the prop mean what it says.
"+ New" answers "what next" with "type here", so pressing it should leave you typing. The rich input is lazy-mounted, so the focus lands a frame later — on the click's own tick the handle can still be null.
The send button was a lime puck. It squares off to the control radius everywhere — the composer is a rounded rectangle and this was the one round thing on it — and on Home it takes the primary rather than the composer accent, since the page has a single thing to press. The fill is re-toned by the CONTAINER through three CSS variables, not by a prop: the button sits five levels below any host that might want a different one, and threading a colour down that chain is how one control becomes two. Unset, it is the accent it has always been, so chat and the playground are untouched. Home hands it `colorPrimary`, which already carries the brand's own per-theme answer — ink on light, yellow on dark. The tab row's rule drops to the divider step so it stops competing with the active tab's underline, "New" says what it makes, and the column's horizontal inset comes in.
The tab listed all 28 templates in a 330px scroller, so it read as the gallery rather than a suggestion — and the gallery it ends with was 23 rows below the fold. Five, then the row that opens the rest. The count on that row stays the full catalogue's, not the shortlist's.
The mask was computed only in the scroll handler, so a list that already overflowed on arrival showed a hard edge until you touched it — the fade IS the signal that there is more below, and it was absent exactly when it had to do its job. It is measured on every render now (idempotent, so that is safe) plus on window resize. A ResizeObserver would not do: the content grows inside a capped box without the box ever resizing. The agent glyph also drops to 16px for room inside its tile. The tile stays 34px — shrinking that would take the row's left edge out of line with the tab above it.
The hover fill stopped 6px from the tile, so pointing at a row read as clipping it rather than selecting it. The list now bleeds 8px past the column on each side and hands that straight back as row padding: the fill gets 14px around the tile while the tile itself does not move — it is what the tab above it lines up with. The bleed sits on the wrapper rather than the rows, so nothing overflows the scroller. The tab row takes it back with a matching margin: rows may run wide, but a rule that did would sit proud of the composer.
Which agent the message is aimed at is a standing fact about the composer, but in the footer it shared a row with the actions you take on this one message. It moves to a band docked under the input, tucked beneath the composer's radius so the two read as one object. It NAMES the agent rather than picking it: the list below is the picker, and a dropdown here was a second way to do the same thing that also hid that the rows do it. The composer no longer holds a selection of its own — the host owns it — so the local state and its onAgentChange go. The dock takes a stronger fill than the composer, not a weaker one: as a lighter step it read as a mismatched slab behind the input rather than a band attached to it.
The dock sat below the composer as its own filled box, so the two read as separate objects however the fill was tuned. It slides behind instead — transparent, carrying the composer's own border token, pulled up under it — so only its sides and foot show and the composer keeps its shape. The composer is lifted with z-index rather than the dock pushed back: a negative z on the dock took it behind the page's own background and it disappeared entirely.
The dock carried the composer's own border weight, which read as a second box below it rather than a shelf. Also drops its top border: the composer's fill is 4% opaque, so a line tucked under it shows straight THROUGH rather than behind — the composer's own bottom edge is the rule between them.
An ✕ at the end of the dock clears the bound agent. Clearing does not leave the composer aimed at nothing — "no agent" is not a state it can send from — so it falls back to create mode, which is why that state carries no ✕ of its own. Binding one of the rows below replaces it. That inverts where the ✕ lived: cancelling create was the only exit before, and it left the composer pointing at whichever agent happened to be first rather than at what you had chosen. The dock's label also drops to 12px regular — at 13px medium it read as a control rather than a statement of what the composer is aimed at.
The ring wrapped the whole composer-plus-dock, so the sweep traced a border around the pair. It marks the box you are typing in, so it moves inside HomeTaskComposer and wraps the input alone — the dock keeps its own soft outline and stays still behind it.
The dock sat 1px proud on each side, so its corner showed as a step beside the composer's rounded bottom. The ring wrapper insets the composer by its own 1px padding; the dock now takes the same inset. It also tucks 24px behind rather than 12px, with the padding restored on the other side so the agent's name does not move — the deeper the top edge sits behind the composer, the less of its outline can read as a second box.
The autofocus that puts the caret in the composer also fires its `focus-within` edge, which paints over the ring's 1px rim — the mode's one signal, hidden by the thing that announces the mode. The composer's own border goes transparent while creating, so the sweep IS the border.
Clicking a template built an agent on the spot and navigated away, so you arrived at something whose brief you had never seen. A row now binds the template the way an agent row binds an agent: it names it in the dock, writes its builder instruction into the composer, and waits — the brief is readable and editable before anything is created. That collapses two create paths into one. The template no longer carries its own seed through the host; it contributes only its NAME, and the text in the composer is the instruction whether it was typed or seeded, so `onCreateFromTemplate` goes and `onCreateFromPrompt` takes the name. Its ✕ clears back to a blank create, which is why that state still has none of its own.
…ways scroll The row was a bare anchor, so mobile's basePath was never applied and it 404'd — every other link in this package already uses next/link. The page also scrolled sideways by 8px: overflow-y-auto alone makes overflow-x compute to auto, so the list's hover bleed became a scroll axis. The page scrolls one way now.
The cold-start hold was FirstRunLoading, drawn from what the first-run hero and the OLD two-column Home shared. Home is a centred column now, with a different frame and different gaps, so that hold shifted into it. The skeleton is measured from HomeFocus itself — the 620px column, the 26px gaps, the 114px composer with its dock below, the 34px tiles — and takes the same frame string as the page, which HomeScreen now names once instead of writing twice. Trade-off worth knowing: a brand-new project briefly holds on a Home-shaped skeleton before resolving to the first-run hero. That is the uncommon path, and the common one no longer shifts.
The agents list atom exposes no `refetch` — its state is data/isPending/ isError only — so the error slot's retry invalidates the list cache instead, the same call `useNewAgentAction` makes after a create. The rest is format and import order.
The mic is the same VoiceInputButton and useVoiceComposer the chat composer already runs, with the recording takeover and the permission notice that go with it — not a second voice affordance. One difference on Home: a take is always an ATTACHMENT, never a message of its own. There is no conversation here to send it into, so both the attach and the send-outright paths park it in the tray and the composer's text is still what gets sent. The attach button becomes a plus, which reads as "add something" rather than naming one kind of thing. It is the app's single attach control, so this changes it in chat and the playground too.
Recording a voice message has always had a waveform; dictation had no signal at all — words simply appeared. The obstacle was the source: the Web Speech API hands back transcripts and no audio, so there was nothing in that pipeline to draw. `useDictationAnalyser` opens a stream of its own purely to be measured — an analyser, no MediaRecorder, nothing captured or kept — and the existing RecordingWaveform draws it. The cost is a second microphone consumer while dictating, and with it a second OS mic indicator; the stream is held only for the length of a dictation, because one kept open between sessions would look like the app is always listening. Failure is silent: a refused stream leaves the wave absent and dictation untouched. The recogniser owns the permission conversation and has its own error channel, so a second message about a decoration is noise. The wave lands in the row the shortcut hints already vacate while dictating, so nothing shifts. RichChatInput takes it as a ReactNode rather than an analyser — the audio belongs to whoever owns the mic, and @agenta/ui sits below @agenta/chat and cannot reach the waveform anyway. `getAudioContextCtor` moves out of useAudioRecorder to be shared: two features build this graph now, and a second copy of the webkit prefix dance is a second place for Safari to be forgotten.
The hints were faded out for dictation but still mounted, and a mounted row of hints still holds its width — so the wave drew in whatever was left over instead of across the composer. They are unmounted while it runs; the reason to keep them mounted (never reflowing the row) does not apply when something else is taking their place. The attach button goes too. The row belongs to the wave while a voice is coming in, and attaching a file mid-utterance is not a thing anyone is doing.
Bar thickness fell out of the slice count and a fixed 2px gap, so it grew with the strip — wide bars on a wide composer. It is stated now, and the leftover width becomes the gaps, so the bars read as fine lines at any size while the strip still spans the full row. Shared with the recording takeover's waveform, which gets the same.
…trol The button throbbed for the whole of a dictation. With the waveform now beside it saying a voice is coming in, a second thing pulsing made the stop control read as a status light rather than something to press. `highlighted` still tints the icon; only the animation goes.
The editor's ceiling was hardcoded at max-h-40, so a long draft on Home grew the composer until the list below it went past the fold. Home caps at 112px instead. A prop rather than a smaller default, mirroring the minHeightClassName already there: a chat dock has nothing under it to push and keeps the taller ceiling, while a surface with a page behind the composer wants a lower one.
The dock names the bound agent, which is exactly what the hold does not know yet — and on a first run there is no agent to name, so the bar promised a row that never arrives. The composer block stands alone.
… row The marks each carried a ring in `colorBgContainer` to fake a gap, but they sit on a transparent row over the page — and over the hover fill on top of that — so the ring was a bright halo in both themes, taking a bite out of its neighbour. It is gone; the overlap alone separates them. That overlap was a fixed 4px tuned for a 16px mark, which is a quarter of the 13px the row now draws. It scales with the mark instead. The row's marks also drop to 13px — the template CARD keeps 16, which is what that size was chosen for, so it is a prop rather than a new default. And a selected agent or template gets the check the agent picker already uses: a tinted row on its own reads as hover on whatever you are pointing at. The tabs close 4px toward the composer while we are here.
`selectedAgentId` fell back to `agents[0]` whenever nothing was picked, so selecting a template put a check on the first agent — a selection nobody made, contradicting the dock right above it. Creating binds no agent, blank or from a template, so no row is marked.
The list arrived in whatever order the query returned, so an agent made months ago sat above one created a minute earlier. Sorted on created_at. This also moves what the composer binds by default, since that is the head of the same list — the agent you just made is now the one it is aimed at.
Morning ran from midnight, so the small hours greeted someone at the end of a long day as if they were starting one. 05:00-11:59 morning, 12:00-16:59 afternoon, 17:00-04:59 evening.
A username is a handle — `ashraf_chowdury99` greeted somebody by their
login. Separators become spaces and digits go, so it reads as "ashraf
chowdury".
An email is dropped rather than cleaned: the local part of one is not a
name, and greeting somebody by their address reads as a mail merge. A
handle that cleans down to nothing ("_99_") drops the eyebrow too, the
same as no profile.
HomeTaskComposer has a second host — the agent overview's AgentComposer — and Home's additions had been welded into it, so that page silently grew a microphone, a recording takeover and Home's 112px editor ceiling. Voice is a `voice` prop now, off by default, and the ceiling is the prop it was always meant to be. The agent page is back to a plain composer. The composer's binding was three pieces of state — `creating`, `agentId`, `template` — encoding one fact. A bound template with `creating` false was representable and meaningless, and every handler reset two or three of them. One `Binding` union replaces them, so the invalid states cannot be written. "Which agent is bound" was resolved twice, in HomeFocus and again in the composer, with the same fallback written two ways — which is exactly how the list came to check an agent nobody had chosen. HomeFocus resolves it once and hands the answer to both. Also: the dock's two branches were the same row with a different tile, so they are one `DockLabel`; `composerExtraPrefix` and `onStartError` had no caller and are gone (the latter was swallowing rejections silently); the templates catalogue arrives as a prop like the agents beside it rather than being imported into the component; and the dock comment, four prop docs and two `[[HomeComposer]]` links all described code that no longer exists. The mic and plus tighten to one cluster while we are here.
Back to a paperclip from the plus, but not the diagonal default — `PaperclipHorizontal` turned a right angle stands exactly vertical, where rotating the diagonal one only approximates it.
Rotated +90 the clip's hook sat at the foot; -90 puts it at the head, which is the way round a paperclip is drawn.
PaperclipHorizontal is drawn as a flat wide clip; standing it up made a stretched pill rather than a paperclip. The diagonal one is the shape that icon is designed to be read at.
A project with no agents got a separate first-run hero: its own page, its own composer, its own copy, teaching a layout you then never saw again. Home does that job with one fewer page to learn. It opens on the templates tab with the composer already describing an agent, and "Your agents" is not offered until there is one. So FirstRunScreen, FirstRunComposer, their copy and their states are deleted, and `homeSurface` moves to the home feature it now serves: it only decides whether we know enough to draw Home yet. Its two real rules survive — an error is never read as emptiness, and an empty list that is still resolving holds rather than flashing the wrong tab. The tab and the composer's mode are read once at mount, which is when the answer is known: Home does not render until the list has resolved.
…oster Binding an agent is half the sentence, so the caret goes where the other half is typed. Templates and "+ New agent" already did this; agent rows did not, which made them the one row in the list that left you nowhere. Reading the roster once at mount was also wrong. The list can arrive empty and fill a moment later, and the snapshot then left the page stuck on templates with a full roster sitting behind the other tab — which is exactly what it did on a cold load. The tab and the binding DERIVE from the roster now, with an explicit choice outliving it: null means follow the data.
4362add to
094afd9
Compare
Two of these were mine to begin with. Hiding "Your agents" for an empty roster also hid the tab the loading, empty and error slots render in, so a FAILED fetch showed the templates list with no message and no retry — the one case `resolveHomeSurface` keeps on Home precisely so it can be retried. The tab now exists whenever the host has something to say there. And the review pass that deleted `onStartError` as dead API took the try/catch with it. `RichChatInput` neither awaits nor catches `onSubmit`, so a rejecting host became an unhandled rejection. The rest CodeRabbit found on its own: - A template's instruction was dropped when it was picked before the lazy `RichChatInput` chunk resolved: one frame was enough on a warm load and not on a cold one, and create mode then submitted empty text with only the template's name. The seed retries until the handle exists, bounded. - A cancelled navigation RESOLVES false rather than throwing, so catching alone cleared the attachments while the task sat unplayed in the stash. The same hazard `useNewAgentAction` already documents. - 64 bars at 2.5px with a 1px gap need 223px, and the floors pushed the last bar off the right edge below that. Both minimums go. - An analyser that cannot be built leaves the microphone open for the length of the dictation with nothing measuring it. - `outline-none` with no replacement left the rows, tabs, New agent and the retry button with no visible focus for a keyboard.
Context
Mobile's Home was six competing headers. A hero and a composer, then Sessions and Automation runs down the main column, with Your agents, Next triggers and Usage stacked in a rail beside them. None of it was the page's own job. Every panel summarised a page that already exists, so Home read as a table of contents rather than a place to start work.
This rebuilds it as the design in
Home redesign.dc.html(option 3c): one centred column that keeps one job and one list.Changes
Home is now a greeting, a question, a composer, and a single list region that is either your agents or the starter templates. The two never occupy the page at once.
The composer has two modes and one input. In task mode it runs work with an agent that exists. "+ New agent" flips the same box into create mode rather than navigating: the placeholder changes, the caret lands in the input, and a conic ring sweeps the border while it is there. Which agent the message is aimed at is named in a dock under the input, not picked there. The list below is the picker, and a dropdown in the dock was a second way to do the same thing that also hid that the rows do it.
A template row selects rather than creates. Clicking one used to build an agent on the spot and navigate away, so you arrived at something whose brief you had never seen. It now binds the template in the dock and writes its builder instruction into the composer, where it can be read and edited before anything is created. That collapsed two create paths into one: the template contributes only its name, and the composer's text is the instruction whether it was typed or seeded.
What the composer is aimed at is one
Bindingvalue rather than three flags:Three separate
creating/agentId/templatestates could contradict. A bound template withcreatingfalse was representable and meaningless, and it is what made the list check an agent nobody had chosen.Sessions, automation runs, next triggers and usage are gone from Home. They stay on the pages that own them.
Where the code lives. The composition is
@agenta/home-ui(HomeFocus,HomeEntityList,HomeGreeting, and the two modes onHomeTaskComposer), because that package exists so Home has one shape.web/mobilesupplies only its routing verbs, its designed states, and the keyframes the ring animates through. Desktop'sStripHomestill rendersHomeOverviewand is untouched.Shared components this also changes
HomeTaskComposer,ChatComposerandRichChatInputare shared, so a few things reach chat, the playground and onboarding:colorPrimary(ink on light, brand yellow on dark) instead of the composer accent, via three CSS variables the container sets. Unset, every other composer keeps the accent it has always had.useDictationAnalyseropens a stream purely to be measured (an analyser, noMediaRecorder, nothing captured or kept). A refused stream leaves dictation working with no wave and says nothing, because the recogniser owns the permission conversation.LogoMarksgains astackedprop. Its ring was painted incolorBgContainerwhile the row is transparent over the page, so stacked marks carried a bright halo that bit into their neighbour in both themes.AgentPicker's trigger classes merge withcn.triggerClassNamewas concatenated, so a host override only won on stylesheet order.Tests
Type-check and lint pass on
@agenta/ui,@agenta/chat,@agenta/home-ui,@agenta/entity-uiand@agenta/mobile. The five remaining mobile lint warnings are pre-existingexhaustive-depsin files this branch does not touch.Verified in the running app at several widths and in both themes: greeting, create mode and its ring, template select and seed, the tab switch, the list's fade mask, row binding, and the agent page regression below.
Two things I could not verify here. The Browser pane blocks microphone access, so the dictation waveform and the recording takeover are wired and type-checked but never watched running. And
resolveHomeSurfaceshort-circuits its loading hold whenever the agents list is cached, which it always is on a real account, soHomeSkeletonis measured fromHomeFocusrather than seen.What to QA