SmartContracts are Pickles instances that all have to be recursively verifiable by a single circuit, the zkApp transaction circuit (which lives on the Mina node). In particular, all SmartContracts must have the same public input/output shape. That's one of a couple of things which SmartContract abstracts away: It defines the input/output shape for you (here: https://github.com/o1-labs/o1js/blob/bc945a0a4662bee66cde54fe47ef2bbe74185703/src/lib/zkapp.ts#L683-L685).
The SmartContract public input is actually two field elements: One is a hash of the account update of the SmartContract itself, and the other is a hash of all its child account updates. And SmartContract adds a little wrapper circuit to your method which performs this hashing, so that from within the contract you can just define the account update and its children directly and don't worry about hashing them / connecting them to the public input.
This is where SmartContract is inherently domain-specific: it is all about writing an account update and its children. If you don't care about the account update, there's no reason to use it over ZkProgram.
Apart from the public input wrapper, what SmartContract adds is mostly a DSL-like API to create the account update and its children from within the method. APIs like this.state.set() , this.state.requireEquals(), this.reducer.dispatch() etc, under the hood are all just mutating various fields on the account update.
One which is quite involved is the "calling other contracts" API (otherContract.someMethod()). Under the hood, it creates a child account update for the otherContract and adds a little circuit which proves that the someMethod() input/output arguments are consistent with what happens in the otherContract proof. Calling other contracts would be pretty hard without that DSL, so in a sense, SmartContract enables the feature of composability.
I hope that gives you an idea!