Skip to content

feat(mobile): configure the controlled device's default model roles - #3378

Open
YodonTan wants to merge 14 commits into
GCWing:mainfrom
YodonTan:feat/mobile-model-role-config
Open

YodonTan wants to merge 14 commits into
GCWing:mainfrom
YodonTan:feat/mobile-model-role-config

Conversation

@YodonTan

Copy link
Copy Markdown
Contributor

What

Lets a phone configure the controlled desktop's default model roles: 主力模型 / 快速模型 / 多模态模型 (host ai.default_models.primary / fast / image_understanding). Entry: the settings sheet's 当前远程控制 device list — the connected control target gets a trailing 模型设置 pill (offline rows: none; older hosts: greyed + 升级 badge + reason). The whole flow lives inside the settings sheet as two in-sheet pages: 设备模型设置 → per-role picker.

How — typed verb, zero host secrets

  • New relay verb SetModelDefaults {primary?, fast?, image_understanding?} (whole-object write of the three slots; null = follow-primary / unconfigured) answered by ModelDefaultsUpdated carrying all six default_models slots after reconcile — the client always renders the echoed/re-read mapping, never the request (primary:null is re-filled by the device's first enabled text-capable model).
  • New capability device_model_defaults_v1; older hosts cannot parse the verb, so the capability gates the entry, the pages, and every read/write, degrading loudly (never version-guessing).
  • Host write merges the three slots under one config write lock (update_config("ai", …)): host-only slots (search / image_generation / speech_recognition) are preserved, concurrent desktop saves can no longer be lost, and reconcile still runs before persist. Unknown model ids are rejected with the same wording as normalize_remote_model_selection.
  • Security posture: the response is a role-id map only — no api_key, endpoint, or provider credential fields (pinned by a contract test). This deliberately does NOT ride the host_invoke config passthrough, whose read family echoes plaintext provider keys (pre-existing, out of scope here).

Client

Wire layer (RemoteModels / RemoteCommandFactory / RemoteSessionManager) → RemoteModelDefaultsController (capability/connection gating, busy lock, write-then-reread with generation fencing — no optimistic updates) → settings entry pill → DeviceModelDefaultsPanel + ModelRolePickerPanel (fast picker: pinned 跟随主力 + text_chat candidates; multimodal: pinned 未配置 + image_understanding candidates with the 图片理解 badge only there; primary: required, no null row; candidate order follows the catalog). Unavailable / reading / failed states are stated on both pages with host error text verbatim and retry.

Verification

  • Node: 472 tests / 471 pass / 1 pre-existing unrelated failure (workspace-editor.test.cjs, fails identically on main). New: device-model-defaults 17 + model-role-pages 21 — gating matrix, six-slot parse, write-then-reread ordering, picker filters/pinned-null rules, unavailable/failed picker states, capability chain.
  • Rust: remote_connect_contracts 78, remote_connect::tests 22, core device 4 (incl. a real-ConfigService behavioral test: host-only slots survive, primary:null re-filled, rejected id leaves the file byte-identical). rustfmt clean.
  • Gates: harmony:architecture, mobile:ui:check, i18n lockstep (both locales + rawfile) green; clean-rebuild CompileArkTS 0 errors. theme:color-audit:all is red on the design-lab surface only (1 fallback + 7 css-private vars against a zero baseline) — reproduced byte-for-byte from origin/main's own design-system/apps/design-lab tree and its baseline, so it is a pre-existing main-side failure: this branch changes no design-system/** or scripts/** file.
  • Emulator evidence (untracked artifacts/): entry enabled + degraded pills (layout dump proves the enabled id/opacity/clickable), both pickers light+dark, writing-lock, updated-with-readback, degraded, empty catalog, picker unavailable/failed states.

Not verified

  • Live end-to-end against a real desktop over the relay (live-host verification remains blocked) — the host verbs need a desktop build with this change; until then the phone pairing round trip (write → reconcile echo → re-read) is covered by contract/core tests and mock-driven UI states only.
  • Wide/foldable postures not exercised. Cross-process app.json writes (desktop + CLI simultaneously) are serialized by file replacement only — pre-existing, unchanged here.
  • Overlaps open PRs feat(mobile): stream swarm subagent task cards from the host record stream #3327 / feat(mobile): command palette with skills picker for remote sessions #3335 on shared files (EntryAbility, RemoteModels, RemoteSessionManager, string.json, …); whoever merges later rebases — regions are disjoint except import/scenario-list unions.

Tant added 14 commits October 14, 2026 01:05
The phone can now read and write the desktop's model role defaults over a
typed verb instead of a config passthrough.

- device_model_defaults_v1 capability plus the three-slot command fields:
  the whole object always travels, and null is a value, so a host can never
  read an absent key as "leave unchanged".
- RemoteDefaultModels now projects all six host slots as string|null, and
  setModelDefaults answers with the reconciled projection. A host that only
  acknowledges the command is followed by an authoritative device read,
  mirroring the permission-mode pattern.
- The composer's UI projection keeps its "absent means nothing configured"
  shape, so the wider wire type does not leak into presentation.
The device settings page needs one owner for the gate, the read and the write,
so the pages only render what it reports.

- RemoteModelDefaultsController reads get_model_catalog without a session id
  (device scope), gates on device_model_defaults_v1 plus the live link, locks
  while a write runs, and never updates values optimistically: a write is
  followed by an explicit re-read and only that read may change what the page
  shows. A result that lands after the gate changed is dropped.
- DeviceModelSettingsEntryPolicy decides the entry, its greyed state, its
  upgrade badge and its one-line reason together, so the row and the reason can
  never disagree; the entry belongs to the connected control target only.
- Both locales gain the entry, degrade, scope and status keys, and the node
  suite pins the command shape, the six-slot response, the gate matrix, the
  busy lock, the write-then-read order and the two distinct failures.
The connected desktop's row now offers its own model role settings instead of a
re-connect action, and the sheet carries one more page for it.

- The row's trailing pill is the same 14-radius SOFT capsule as the connect
  action, because it is the same slot; an offline row keeps no entry at all,
  and an older desktop keeps a greyed pill with an upgrade corner badge whose
  tap explains itself in place.
- The sheet page id 'deviceModels' is registered now and reachable from the
  row. The pages of that page are still a placeholder marked TODO, so the
  navigation, the sheet state and the degrade path are real before the panels
  land.
- Settings now own typed device-model actions (device-level catalog read and
  whole-object write) next to the permission-mode pair, so the pages can build
  the controller client without reaching into the transport.
A controller could read a device's `ai.default_models` through
`get_model_catalog` but never write it. Add the typed write path instead of
routing a raw config mutation: `SetModelDefaults` writes the three managed role
slots as one object (an absent field and an explicit `null` both clear a slot),
`ModelDefaultsUpdated` answers with the host's post-reconcile six-slot mapping,
and `device_model_defaults_v1` lets a caller negotiate the verb before offering
it. The response is a role mapping only, so no model credential can travel on it.

Validation rejects a slot that names a model the device does not configure and
rejects the semantic session selectors (`primary` / `fast` / `auto` / `default`).
Enablement and capability validity stay with the config reconcile, whose repair
the response reports instead of echoing the request.
Implement `RemoteSessionRuntimeHost::set_model_defaults` so a controller's
primary / fast / image-understanding choice persists on the device it controls.

The write merges the three managed slots over the current `ai.default_models`
and stores the whole object through the config service, so the host-only slots
(`search`, `image_generation`, `speech_recognition`) keep their device values and
the existing reconcile runs. The reply is read back from the reconciled config:
a slot pointing at a disabled model or one without the capability its role needs
reports the repaired value rather than what the request asked for. An id the
device does not configure is rejected before anything is persisted.
The settings pages need to know which models a role may be set to, what a role
row says, and how the picker is entered and left before any of that is drawn.

- `ModelRoleCandidatesPolicy` filters the desktop catalog per role (enabled and
  capable, desktop order preserved), names the pinned no-model value of each
  role, derives a role row's value and hint, and locates a role's resolved model
  with the fast slot falling back to the primary.
- `DeviceModelDefaultsSheetState` holds the controller for the connected desktop
  and owns the write's feedback window, the busy lock and the read-back rule:
  a value only ever changes through a read.
- `SettingsSheetState` gains the sheet's second level (`main` -> `deviceModels`
  -> `modelPick`) and resets the model state when the sheet closes.
Replaces the device model placeholder with the two real pages.

The settings page shows the desktop's summary, one row per role (primary always
resolved, fast saying so when it follows the primary, multimodal naming the
capability it needs), the three scope lines, and the writes' own feedback: the
whole sheet locks while a write is out, the confirmed value is reported for a
moment after the read that answers it, and a refusal is shown verbatim with a
retry that repeats exactly what failed. There is no manual refresh.

The role picker is a second level of the same sheet: the pinned no-model row
first, then the desktop's own candidates in its own order, with the multimodal
badge only where the capability is the point. Selecting writes and returns; the
value shown afterwards is always the one the desktop read back, never the tap.

Both pages read the catalog as the protocol reports it and project models
through the shared presentation mapper, so a model is named here exactly as the
rest of the app names it. A catalog with nothing to offer keeps the rows
tappable and says why the lists are empty; a device that does not support the
feature says so instead of pretending to be empty.

ArkTS notes: the sheet binds its request path as a literal, which ArkTS only
accepts against property-typed members, so the two calls are declared as
`DeviceModelDefaultsRequestPath` rather than as the controller's method-shaped
client interface.
Runs the candidate policy and the sheet state for real over a recorded desktop:
which models each role may take and in whose order, what the pinned no-model
row means per role, what a role row says, the write lock and the read-back, the
refusal and its retry, the stale write a closed sheet drops, and the sheet's
two-level page stack. The pages' own tripwires keep the placeholder gone, the
refresh button absent, and every scenario id reachable.
The design gallery mounts the production settings sheet over a fixture desktop
that speaks the two device-model requests, so the previews exercise the real
panels, the real gate and the real controller — including the write lock, which
only exists while a request is actually out.

Scenarios: device-models (the fixture's own default state), device-model-pick-
fast, device-model-pick-multimodal, device-model-pick-primary,
device-model-writing, device-model-updated, device-model-degraded and
device-model-empty. The family carries one `-dark` suffix for the colour mode,
so every state is reachable in both.
The remote device-model verb read `ai` and then wrote a whole
`ai.default_models` object built from that snapshot, so a desktop-side save
committed between the two locks was silently reverted — the hazard the config
service warns about for a get_config-then-set_config pair.

Merge, validate, and store now happen inside one `update_config("ai", ...)`
closure: the id check reads the same `ai.models` the manager will persist
against, the host-only slots are carried over from the same locked read, and the
manager's own `set` path still reconciles the object before it commits. A
rejected id returns from the closure without touching the stored section, so it
is never a partial write.

The verb also documents what clearing actually means per slot: unset `fast`
follows primary and unset `image_understanding` stays unconfigured, while unset
`primary` is re-filled by the reconcile with the device's first enabled
text-capable model.

Tests: the write-path seam is pinned to the single-closure call, and a new
behavioral fixture runs the real config service and reconcile to cover the
device-owned slots surviving the write, the persisted result, the cleared
`primary` fallback, and the rejected-id no-op.
The picker could be open when the desktop stopped answering it, and it drew the
last roles it had as if the device had just reported them: the pinned row was
checked — "fast follows the primary" on a desktop the phone could no longer
reach — and an empty list was blamed on the catalog instead of on the link.

`ModelRoleCandidatesPolicy.pickerView` now decides one picker frame from one
device state: whether the reason card is shown, whether a retry can repeat the
read, whether the rows are drawn, and whether those rows are the device's own
answer at all. A mark and the "no models match this role" line are only drawn
from a list the device answered; a page that has not been read yet says it is
reading rather than blaming a catalog nobody has seen.

Both pages state the reason in one shared card, with the retry offered only
while the device can still answer, so a closed gate does not grow a button that
cannot do anything.

The connected desktop's model settings entry was also never exercised: the
settings fixture advertised no host capabilities, so every shot of the device
row could only ever show the greyed entry with its upgrade badge. The fixture
now advertises the command, the pill carries its state in its id, and a test
pins the capability chain from the state the shell reads to the branch the row
draws.

Previews: device-model-degraded-pick and device-model-pick-failed add the two
picker states that have no list.
…e-config

# Conflicts:
#	src/apps/mobile/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets
#	src/apps/mobile/harmonyos/entry/src/main/ets/services/RemoteSessionManager.ets
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