diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx
index 2673b57e527..3b62b787216 100644
--- a/mobile/app/h/[hostId]/index.tsx
+++ b/mobile/app/h/[hostId]/index.tsx
@@ -44,6 +44,7 @@ import {
import type { RpcSuccess } from '../../../src/transport/types'
import { StatusDot } from '../../../src/components/StatusDot'
import { NewWorktreeModalController } from '../../../src/components/NewWorktreeModalController'
+import { NewWorkspaceFab, FAB_SIZE } from '../../../src/components/NewWorkspaceFab'
import { MobileRepoIcon } from '../../../src/components/MobileRepoIcon'
import { WorktreeListRow } from '../../../src/components/WorktreeListRow'
import { useNow } from '../../../src/hooks/use-now'
@@ -1099,17 +1100,6 @@ export function HostScreen({
/>
-
-
-
-
setShowSearch((s) => !s)}>
{showSearch ? (
@@ -1187,7 +1177,9 @@ export function HostScreen({
// above the Samsung 3-button nav / iOS home indicator.
contentContainerStyle={[
styles.list,
- { paddingBottom: spacing.lg + insets.bottom },
+ // Phone shows a floating "+" button bottom-right; reserve room so the
+ // last row stays tappable above it. Embedded sidebars keep the toolbar +.
+ { paddingBottom: (embedded ? spacing.lg : FAB_SIZE + spacing.xl) + insets.bottom },
isWideLayout &&
!embedded && { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' }
]}
@@ -1245,6 +1237,11 @@ export function HostScreen({
/>
)}
+ {/* Floating "new workspace" button — phone only; embedded sidebars keep the toolbar +. */}
+ {!embedded && (
+
+ )}
+
void
+ disabled?: boolean
+}
+
+// Phone-only floating "+" for creating a workspace. Absolutely positioned so it
+// never intercepts list row taps, and lifted above the home indicator.
+export function NewWorkspaceFab({ onPress, disabled }: NewWorkspaceFabProps): React.JSX.Element {
+ const insets = useSafeAreaInsets()
+ return (
+ [
+ styles.fab,
+ { bottom: spacing.xl + insets.bottom },
+ pressed && styles.fabPressed,
+ disabled && styles.fabDisabled
+ ]}
+ onPress={onPress}
+ disabled={disabled}
+ accessibilityRole="button"
+ accessibilityLabel="New workspace"
+ hitSlop={8}
+ >
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ fab: {
+ position: 'absolute',
+ right: spacing.lg,
+ width: FAB_SIZE,
+ height: FAB_SIZE,
+ borderRadius: FAB_SIZE / 2,
+ alignItems: 'center',
+ justifyContent: 'center',
+ // Crisp near-white surface + dark icon: high contrast against the dark canvas
+ // reads as the primary action while staying monochrome (STYLEGUIDE: color is
+ // for state). Tight shadow avoids the muddy halo that made it look disabled.
+ backgroundColor: colors.surfaceBright,
+ shadowColor: '#000',
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ shadowOffset: { width: 0, height: 2 },
+ elevation: 4
+ },
+ fabPressed: {
+ backgroundColor: colors.textPrimary
+ },
+ fabDisabled: {
+ opacity: 0.5
+ }
+})
diff --git a/mobile/src/theme/mobile-theme.ts b/mobile/src/theme/mobile-theme.ts
index 5583b73f078..76b14415a32 100644
--- a/mobile/src/theme/mobile-theme.ts
+++ b/mobile/src/theme/mobile-theme.ts
@@ -12,6 +12,11 @@ export const colors = {
textSecondary: '#888888',
textMuted: '#555555',
+ // Crisp near-white surface for the single primary action on a screen (the
+ // worktree FAB). Brighter than textPrimary so it reads as a solid button, not
+ // disabled chrome, while staying monochrome (STYLEGUIDE: color is for state).
+ surfaceBright: '#f5f5f5',
+
accentBlue: '#3b82f6',
statusGreen: '#22c55e',