Skip to content

[feat] Rebuild mobile Home as one composer over one list - #6732

Open
ashrafchowdury wants to merge 39 commits into
feat/mobile-sessions-redesignfrom
feat/mobile-home-focus
Open

[feat] Rebuild mobile Home as one composer over one list#6732
ashrafchowdury wants to merge 39 commits into
feat/mobile-sessions-redesignfrom
feat/mobile-home-focus

Conversation

@ashrafchowdury

Copy link
Copy Markdown
Contributor

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 Binding value rather than three flags:

{kind: "agent"; id: string | null} | {kind: "template"; template} | {kind: "new"}

Three separate creating / agentId / template states could contradict. A bound template with creating false 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 on HomeTaskComposer), because that package exists so Home has one shape. web/mobile supplies only its routing verbs, its designed states, and the keyframes the ring animates through. Desktop's StripHome still renders HomeOverview and is untouched.

Shared components this also changes

HomeTaskComposer, ChatComposer and RichChatInput are shared, so a few things reach chat, the playground and onboarding:

  • The composer send button squares off to the control radius. It was the one round thing on a rounded rectangle. On Home it also takes 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.
  • The editor's max height becomes a prop. Home caps at 112px because it has a list underneath to push; a chat dock keeps the 160px default.
  • Dictation gets a waveform. The Web Speech API returns transcripts and no audio, so useDictationAnalyser opens a stream purely to be measured (an analyser, no MediaRecorder, nothing captured or kept). A refused stream leaves dictation working with no wave and says nothing, because the recogniser owns the permission conversation.
  • LogoMarks gains a stacked prop. Its ring was painted in colorBgContainer while 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 with cn. triggerClassName was 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-ui and @agenta/mobile. The five remaining mobile lint warnings are pre-existing exhaustive-deps in 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 resolveHomeSurface short-circuits its loading hold whenever the agents list is cached, which it always is on a real account, so HomeSkeleton is measured from HomeFocus rather than seen.

What to QA

  • Open Home. The greeting reads the right time of day (05:00 morning, 12:00 afternoon, 17:00 evening) and your account name with separators and digits stripped.
  • Press "+ New agent". The composer flips to create mode, the ring sweeps its border, and the caret is already in the input. There is no way to cancel it, by design: bind a row instead.
  • Open Templates and click a row. It does not navigate. The dock names the template, the composer fills with its instruction, and the row is checked.
  • Switch back to Your agents with a template bound. No agent row is checked. This was the bug: it used to check the first agent.
  • Click an agent row. The dock names it, the row is checked, and the ✕ clears back to a blank create.
  • "Browse all 28 templates" opens the gallery. It 404'd before, because a bare anchor skips mobile's basePath.
  • Regression, agent overview page: open any agent. Its composer has no microphone and its editor still grows to 160px. Home's additions were briefly welded into the shared component; this is the fix.
  • Regression, chat and the playground: the send button is squarer but still the brand lime, and the attach button is still a paperclip.
  • Both themes on all of the above. The design was authored light-only.

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 10, 2026 12:55pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Summary

Summary by CodeRabbit

  • New Features

    • Redesigned the mobile home screen with personalized greetings, task and agent-creation modes, agent/template browsing, and starter templates.
    • Added clearer loading, empty, and retry states for home content.
    • Added optional agent naming when creating an agent from a prompt.
    • Added live dictation waveform visualization and improved voice-input behavior.
    • Added compact, stacked provider marks and refreshed composer visuals.
  • Bug Fixes

    • Improved waveform sizing across different display widths.
    • Added Safari-compatible audio handling and reduced-motion support for composer animations.
    • Improved home-screen loading behavior when agent data is unavailable.

Walkthrough

The 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.

Changes

Home focus experience

Layer / File(s) Summary
Home focus components and composer
web/packages/agenta-home-ui/..., web/packages/agenta-ui/...
Adds the focused home surface, agent and template tabs, greeting, two-mode composer, template marks, stacked logos, and shared input styling.
Home handoff and agent creation
web/mobile/src/features/home/useHomeHandoff.ts, web/mobile/src/features/agents/useNewAgentAction.ts, web/mobile/src/features/home/HomeComposer.tsx
Moves session creation, attachment staging, task navigation, pending-task cleanup, and named prompt-based agent creation into useHomeHandoff.
Mobile home integration and states
web/mobile/src/features/home/HomeScreen.tsx, web/mobile/src/features/home/states/*, web/mobile/src/features/onboarding/*, web/mobile/src/styles/globals.css
Renders HomeFocus, removes the first-run flow, adds loading and retry states, and defines create-mode ring animations with reduced-motion support.

Dictation waveform

Layer / File(s) Summary
Audio analysis lifecycle
web/packages/agenta-chat/src/hooks/*
Adds shared audio-context lookup and a dictation analyser that manages microphone streams, analyser nodes, cleanup, and failure handling.
Waveform rendering and composer wiring
web/packages/agenta-chat/src/components/*, web/packages/agenta-ui/src/RichChatInput/*, web/mobile/src/features/chat/Composer.tsx
Passes the analyser through voice and chat composers, renders RecordingWaveform during dictation, adjusts waveform sizing, hides attachments while dictating, and supports configurable editor height.

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
Loading
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
Loading

Merge Risk: 🟡 Moderate · up to 4362a

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)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: rebuilding mobile Home around one composer and one list.
Description check ✅ Passed The description is detailed and directly explains the Home redesign, composer modes, list behavior, shared component updates, testing, and QA scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 28 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mobile-home-focus

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6732.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6732-5052fb7
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-10T13:04:50.116Z

@ashrafchowdury
ashrafchowdury changed the base branch from feat/mobile-automations to feat/mobile-sessions-redesign September 9, 2026 20:25
@ashrafchowdury
ashrafchowdury added this pull request to stack #6736 September 9, 2026 20:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
web/packages/agenta-ui/src/RichChatInput/ComposerSendButton.tsx (1)

44-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the literal hex fallbacks with theme tokens.

--ag-composer-send-fg falls back to #191a0d and --ag-composer-send-hover-bg falls 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-bg gets 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c21639 and a222605.

📒 Files selected for processing (28)
  • web/mobile/src/features/agents/AgentComposer.tsx
  • web/mobile/src/features/agents/useNewAgentAction.ts
  • web/mobile/src/features/chat/Composer.tsx
  • web/mobile/src/features/home/HomeComposer.tsx
  • web/mobile/src/features/home/HomeScreen.tsx
  • web/mobile/src/features/home/states/HomeSkeleton.tsx
  • web/mobile/src/features/home/states/HomeStates.tsx
  • web/mobile/src/features/home/useHomeHandoff.ts
  • web/mobile/src/features/onboarding/FirstRunComposer.tsx
  • web/mobile/src/styles/globals.css
  • web/packages/agenta-chat/src/components/ChatComposer.tsx
  • web/packages/agenta-chat/src/components/RecordingWaveform.tsx
  • web/packages/agenta-chat/src/components/VoiceInputButton.tsx
  • web/packages/agenta-chat/src/hooks/audioContext.ts
  • web/packages/agenta-chat/src/hooks/index.ts
  • web/packages/agenta-chat/src/hooks/useAudioRecorder.ts
  • web/packages/agenta-chat/src/hooks/useDictationAnalyser.ts
  • web/packages/agenta-chat/src/hooks/useVoiceComposer.ts
  • web/packages/agenta-entity-ui/src/agent/AgentPicker.tsx
  • web/packages/agenta-home-ui/src/HomeEntityList.tsx
  • web/packages/agenta-home-ui/src/HomeFocus.tsx
  • web/packages/agenta-home-ui/src/HomeGreeting.tsx
  • web/packages/agenta-home-ui/src/HomeTaskComposer.tsx
  • web/packages/agenta-home-ui/src/TemplateProviderMarks.tsx
  • web/packages/agenta-home-ui/src/index.ts
  • web/packages/agenta-ui/src/RichChatInput/ComposerSendButton.tsx
  • web/packages/agenta-ui/src/RichChatInput/RichChatInput.tsx
  • web/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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Applied the mobile theme's focus recipe (focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50, no ring-offset).

Comment on lines +48 to +58
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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +95 to +97
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +85 to +90
const focusSoon = useCallback((seed?: string) => {
requestAnimationFrame(() => {
if (seed !== undefined) inputRef.current?.setMarkdown(seed)
inputRef.current?.focus()
})
}, [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +229 to +236
onSubmit={async (text) => {
if (creating) {
await onCreate?.({text})
return
}
if (!effectiveAgentId) return
await onStart({agentId: effectiveAgentId, text})
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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-ui

Repository: 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.tsx

Repository: 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web/packages/agenta-home-ui/src/HomeFocus.tsx (1)

11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment 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

📥 Commits

Reviewing files that changed from the base of the PR and between a222605 and fc965a2.

📒 Files selected for processing (10)
  • web/mobile/src/features/home/HomeScreen.tsx
  • web/mobile/src/features/home/homeSurface.ts
  • web/mobile/src/features/onboarding/FirstRunComposer.tsx
  • web/mobile/src/features/onboarding/FirstRunScreen.tsx
  • web/mobile/src/features/onboarding/copy.ts
  • web/mobile/src/features/onboarding/homeSurface.ts
  • web/mobile/src/features/onboarding/states/FirstRunStates.tsx
  • web/mobile/tests/unit/homeSurface.test.ts
  • web/packages/agenta-home-ui/src/HomeEntityList.tsx
  • web/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.

Comment on lines +51 to +52
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. ROW, TAB_BASE and the New agent button share a FOCUS_RING constant using the theme's recipe.

Comment on lines +152 to +154
const hasAgentsTab = agents.length > 0
const showAgents = hasAgentsTab && tab === "agents"
const agentsBody = errorSlot ?? (loading ? loadingSlot : agents.length === 0 ? emptySlot : null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 present agentsBody in hasAgentsTab, so the agents tab exists while a state slot is supplied.
  • web/packages/agenta-home-ui/src/HomeFocus.tsx#L79-L81: default tab to "agents" when errorSlot or loading is 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant