You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Console: offer a scan affordance on text and lookup fields when the mobile shell exposes scan() #29378
Filing category ③: a direct task from the maintainer's card. objectstack-ai/mobile#146 says, verbatim: "Follow-up: after acceptance, the seat files the Console consumer card in objectstack-ai/objectstack (a scan affordance on text/lookup fields when has('scan'))."
What the shell now offers
The shell side merged as objectstack-ai/mobile#170, into mobile main at 92a0a26. The bridge contract lives in mobile docs/HYBRID-SHELL.md, section "Scanning codes".
The call is window.ObjectStackNative.scan(opts?: { formats?: string[] }): Promise<string | null>. Feature-detect it with has('scan').
The shell opens its native full-screen scanner, with torch and close.
The call resolves to the decoded text, or null when the user closes the scanner.
It fails with failed "Camera permission denied", or with failed "A scan is already open" if a second call arrives while one is open.
Bad options are rejected with bad_args.
formats takes expo-camera BarcodeType names (qr, ean13, code128, …). If you omit it, every listed format is read.
Only pages on the configured server's origin can call it, the same rule as every bridge method.
Ask (Console, ui/)
When has('scan') is true, show a scan affordance on text fields and lookup fields. Tapping it calls scan().
Text: insert the decoded value.
Lookup: search with the decoded value.
null means the user cancelled, so do nothing.
Show failed with the Console's existing error toast.
No affordance in a plain browser, or in an older shell where has('scan') is false.
Business reason
Who benefits: field users entering asset tags, product barcodes or document codes on a phone.
If not done: the shell capability ships with no caller, which is a declared-but-unused capability.
Who acts
The domain:ui seat. Triage first-touch decides grade and area. Where to declare the affordance (a generic field-level block, not a product-named widget) is the UI lane's call under its contract rules.
Claim: PM loop round 19 · 2026-11-02T15:30Z
Session: session_01QsR2jUw36sPe8KXr9ANYMv (card thread, os-dev); dispatched by seat session session_01QsR2jV4dWohojjbTJTQq5m
Account: zhuangjianguo
Branch: claude/project-thread-6pv9qk (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29378
Domain: domain:ui
Seat: domain:ui#3
File surface: ui/packages/mobile/src/nativeHost.ts (+ its index export), ui/packages/fields/src/widgets/TextField.tsx, LookupField.tsx (RecordPickerDialog.tsx only if the search entry needs it), a small shared scan button in @object-ui/fields if useful, @object-ui/fieldspackage.json only for a @object-ui/mobile dependency edge if the design note picks it (lockfile regenerated), i18n keys for the button's accessible name, their tests, and a changeset. A design note (where the affordance lives, how fields reaches the bridge, every public export/prop/key added) goes to the seat before any edit (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: yes (new public exports in @object-ui/mobile and @object-ui/fields, new i18n keys; contract review is the seat's own at review)
Metadata-protocol: none
Responsibility: open — recorded at review
Thread-read: none
Premise: holds (at origin/main6fe99f86: NativeHostMethod in ui/packages/mobile/src/nativeHost.ts has no scan; TextField.tsx and LookupField.tsx draw no scan affordance; the shell side merged as objectstack-ai/mobile#17092a0a26).
Serial constraints cleared: none. No other open card or PR touches nativeHost.ts, TextField.tsx or LookupField.tsx that this seat knows of; the os-dev checks open PRs on those files before editing.
Claim amended · 2026-11-02T15:32Z · on the os-dev's design note (at origin/main5f7d554c), before any edit. This adds to claim 6581080956; branch, session and size are unchanged.
Clause-②: yes
Metadata-protocol: none
Design accepted (seat OK): option A, a new @object-ui/fields → @object-ui/mobile dependency edge (mobile depends only on @object-ui/types; plugin-grid, list, calendar and timeline already take it), detection through mobile's existing useNativeHost(). No new context and no Console wiring.
Public surface added (for the seat's contract review):@object-ui/mobile: 'scan' in NativeHostMethod, an OPTIONAL scan?(opts) on ObjectStackNative (older shells lack it), NativeScanOptions { formats?: string[] }, nativeScan(opts?) → NativeCallResult<string | null> via callNative. @object-ui/fields: one optional initialSearch prop on PeoplePicker (seeds the picker: 'search' lookup variant); the scan button itself is internal (not exported), and TextField / LookupField gain no public prop. i18n: fields.scan.button, fields.scan.failed in every locale plus FIELD_DEFAULTS.
File surface widened to ui/packages/fields/src/widgets/PeoplePicker.tsx (the initialSearch prop only), ui/packages/fields/src/useFieldTranslation.ts (FIELD_DEFAULTS), and the @object-ui/i18n locale files for the two keys.
Behaviour fixed by the note: single-line text inputs only (not password, not the multi-row textarea); the value replaces through onChange with no truncation, so validation reports an over-long value; lookup opens its search with the decoded value, and the button is hidden where the target offers no search; failed → toast.error (title the new key, description the shell's message); null or unavailable → nothing; read-only or disabled fields show no button.
{
"issue": 29378,
"status": "done",
"branch": "claude/project-thread-6pv9qk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29493",
"head": "c6569d87",
"session": "session_01QsR2jUw36sPe8KXr9ANYMv",
"premise_still_valid": true,
"summary": "@object-ui/mobile types the shell bridge's scan method: 'scan' in NativeHostMethod, optional scan?(opts) on ObjectStackNative, NativeScanOptions, nativeScan(opts?) through callNative. @object-ui/fields takes a new @object-ui/mobile edge; an internal ScanFieldButton + useFieldScanOffered (useNativeHost) shows a scan button on editable single-line TextField and on LookupField (classic popover, static options, and the picker:'search' variant via a new optional PeoplePicker initialSearch prop) only when has('scan') is true. Text: decoded value replaces through onChange. Lookup: opens its search with the code. null/unavailable: nothing. failed: toast.error with the new key and the shell's message. Two i18n keys in ten locales plus FIELD_DEFAULTS. Design per the seat-OK'd note and claim amendment 6581140256.",
"declarations": { "Clause-②": "yes", "Metadata-protocol": "none", "copied_from": "claim 6581080956 / amendment 6581140256, verbatim into PR body and changeset" },
"files_changed": [
"ui/packages/mobile/src/nativeHost.ts",
"ui/packages/mobile/src/index.ts",
"ui/packages/mobile/src/__tests__/nativeHost.test.tsx",
"ui/packages/fields/package.json",
"pnpm-lock.yaml",
"ui/packages/fields/src/widgets/ScanFieldButton.tsx (new, internal)",
"ui/packages/fields/src/widgets/ScanFieldButton.shellScan.test.tsx (new)",
"ui/packages/fields/src/widgets/TextField.tsx",
"ui/packages/fields/src/widgets/LookupField.tsx",
"ui/packages/fields/src/widgets/PeoplePicker.tsx (initialSearch only)",
"ui/packages/fields/src/widgets/useFieldTranslation.ts (FIELD_DEFAULTS)",
"ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts",
".changeset/29378-shell-scan-fields.md"
],
"public_surface_added": [
"@object-ui/mobile: NativeHostMethod gains 'scan'",
"@object-ui/mobile: ObjectStackNative.scan?(opts) (optional member)",
"@object-ui/mobile: type NativeScanOptions { formats?: string[] }",
"@object-ui/mobile: nativeScan(opts?) resolving NativeCallResult of string or null",
"@object-ui/fields: PeoplePicker prop initialSearch?: string",
"@object-ui/i18n: fields.scan.button, fields.scan.failed (all ten locales)",
"@object-ui/fields package.json: dependency @object-ui/mobile workspace:*"
],
"line_budget": "22 files changed, 551 insertions, 6 deletions vs merge base (lockfile 3 lines); under the 3000-line class (c)",
"tests": "head bf41e3ae: new ScanFieldButton.shellScan.test.tsx 13/13; mobile nativeHost.test.tsx 46/46. Packages: vitest packages/mobile/ packages/i18n/ = 104 files passed, 1567 tests passed 13 skipped; packages/fields/ = 294 passed 1 skipped, 4426 tests passed 7 skipped (lock VERDICT command-exit 0, 694 s). turbo type-check fields+mobile+i18n: 50/50 tasks, VERDICT command-exit 0. After merging main at c6569d87 (main touched none of fields/mobile/i18n/lockfile): the two scan test files 2 files, 59 tests passed. Ablation via scripts/ablation-replace.mjs on ScanFieldButton.tsx: anchor 'return host.has('scan') === true;' -> '(host.has('scan') || true) === true' landed (anchor 1->0, blob db4b436a -> 28645a70), exactly the two older-shell pins red (2 failed, 11 passed), restored blob == HEAD, git diff HEAD empty. Source-resolved through the ui vitest alias, no dist involved. Phone screenshots (390 px, stub bridge, temporary Console Vite entry, deleted): plain browser 0 buttons, shell 2 buttons, text value ASSET-0042, lookup search ASSET-0042 listing 1 row, failed toast 'Could not scan / Camera permission denied'.",
"gates": "dispatch-gates --commands at cfa0fd12: 54 families; 53 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs full pnpm build; it scans root packages/ only). pnpm --dir ui exec vitest run scripts/: 169 files passed 2 skipped, exit 0. --ran reconciliation: 54 accounted, 53 run, 1 NOT-MEASURED, 0 UNRUN. check-sdui-manifest exit 0. ui check:new-line-citations 0 new. eslint on touched files: 0 errors after the fix commit bf41e3ae (warnings pre-existing plus one react-refresh warning on ScanFieldButton.tsx exporting a hook beside the component). PR-body gates on the sent body: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"deviations": [
"Surface widened before any edit by the seat's amendment 6581140256 (PeoplePicker.tsx initialSearch, useFieldTranslation.ts, locale files).",
"Compact (inline grid cell) lookups get no scan button, like the single-select clear control; noted in Acceptance notes.",
"A temporary Console Vite preview entry (ui/apps/console/scan-preview.html + src/scan-preview.tsx) was used for screenshots and deleted; git status clean.",
"check:dual-build-cjs-loads NOT MEASURED locally (exit 3)."
],
"mcp_calls": "4: issue_read x3 (card body, comments twice), get_session x1 (session id). No MCP write tool.",
"api_writes": "2: fleet-write pr_create (PR 29493, read back 7730/7730 bytes identical); this post-stamped comment on issue 29378. git push is not a REST write.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
PR #29493 merged at 2026-11-02T17:05Z as ec2f5d5c. Fixes #29378 closed this card; pm:dispatched removed. Delivered: inside the mobile shell, when the bridge announces scan, editable single-line text fields and lookup fields show a scan button; a text field takes the decoded code through onChange, a lookup opens its search with it, a closed scanner changes nothing and a failure shows the error toast with the shell's message. @object-ui/mobile gains nativeScan / NativeScanOptions (optional scan? on the bridge), PeoplePicker an optional initialSearch, and fields.scan.* keys in every locale (contract review PASS 6582783281). Nothing left on this card.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:ui·pm:queue· 2026-11-02T14:55Z⛔ Not a claim.
scan(). A maintainer-directed follow-up of Bridge:scan()— barcode / QR scanning for Console fields mobile#146, in the Console, sodomain:ui. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Claim: PM loop round 19 · 2026-11-02T15:30Z
Session:
session_01QsR2jUw36sPe8KXr9ANYMv(card thread, os-dev); dispatched by seat sessionsession_01QsR2jV4dWohojjbTJTQq5mAccount:
zhuangjianguoBranch:
claude/project-thread-6pv9qk(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29378Domain:
domain:uiSeat:
domain:ui#3File surface:
ui/packages/mobile/src/nativeHost.ts(+ its index export),ui/packages/fields/src/widgets/TextField.tsx,LookupField.tsx(RecordPickerDialog.tsxonly if the search entry needs it), a small shared scan button in@object-ui/fieldsif useful,@object-ui/fieldspackage.jsononly for a@object-ui/mobiledependency edge if the design note picks it (lockfile regenerated), i18n keys for the button's accessible name, their tests, and a changeset. A design note (where the affordance lives, howfieldsreaches the bridge, every public export/prop/key added) goes to the seat before any edit (stop on breach; explain in the report)Container & model: M,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: yes (new public exports in
@object-ui/mobileand@object-ui/fields, new i18n keys; contract review is the seat's own at review)Metadata-protocol: none
Responsibility:
open — recorded at reviewThread-read: none
Premise: holds (at
origin/main6fe99f86:NativeHostMethodinui/packages/mobile/src/nativeHost.tshas noscan;TextField.tsxandLookupField.tsxdraw no scan affordance; the shell side merged as objectstack-ai/mobile#17092a0a26).Serial constraints cleared: none. No other open card or PR touches
nativeHost.ts,TextField.tsxorLookupField.tsxthat this seat knows of; the os-dev checks open PRs on those files before editing.objectstack-fleet commentedon Nov 2, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Claim amended · 2026-11-02T15:32Z · on the os-dev's design note (at
origin/main5f7d554c), before any edit. This adds to claim 6581080956; branch, session and size are unchanged.Clause-②: yes
Metadata-protocol: none
@object-ui/fields→@object-ui/mobiledependency edge (mobile depends only on@object-ui/types; plugin-grid, list, calendar and timeline already take it), detection through mobile's existinguseNativeHost(). No new context and no Console wiring.@object-ui/mobile:'scan'inNativeHostMethod, an OPTIONALscan?(opts)onObjectStackNative(older shells lack it),NativeScanOptions { formats?: string[] },nativeScan(opts?)→NativeCallResult<string | null>viacallNative.@object-ui/fields: one optionalinitialSearchprop onPeoplePicker(seeds thepicker: 'search'lookup variant); the scan button itself is internal (not exported), andTextField/LookupFieldgain no public prop. i18n:fields.scan.button,fields.scan.failedin every locale plusFIELD_DEFAULTS.ui/packages/fields/src/widgets/PeoplePicker.tsx(theinitialSearchprop only),ui/packages/fields/src/useFieldTranslation.ts(FIELD_DEFAULTS), and the@object-ui/i18nlocale files for the two keys.onChangewith no truncation, so validation reports an over-long value; lookup opens its search with the decoded value, and the button is hidden where the target offers no search;failed→toast.error(title the new key, description the shell's message);nullorunavailable→ nothing; read-only or disabled fields show no button.objectstack-fleet commentedon Nov 2, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jUw36sPe8KXr9ANYMv
os-dev-report
{ "issue": 29378, "status": "done", "branch": "claude/project-thread-6pv9qk", "pr": "https://github.com/objectstack-ai/objectstack/pull/29493", "head": "c6569d87", "session": "session_01QsR2jUw36sPe8KXr9ANYMv", "premise_still_valid": true, "summary": "@object-ui/mobile types the shell bridge's scan method: 'scan' in NativeHostMethod, optional scan?(opts) on ObjectStackNative, NativeScanOptions, nativeScan(opts?) through callNative. @object-ui/fields takes a new @object-ui/mobile edge; an internal ScanFieldButton + useFieldScanOffered (useNativeHost) shows a scan button on editable single-line TextField and on LookupField (classic popover, static options, and the picker:'search' variant via a new optional PeoplePicker initialSearch prop) only when has('scan') is true. Text: decoded value replaces through onChange. Lookup: opens its search with the code. null/unavailable: nothing. failed: toast.error with the new key and the shell's message. Two i18n keys in ten locales plus FIELD_DEFAULTS. Design per the seat-OK'd note and claim amendment 6581140256.", "declarations": { "Clause-②": "yes", "Metadata-protocol": "none", "copied_from": "claim 6581080956 / amendment 6581140256, verbatim into PR body and changeset" }, "files_changed": [ "ui/packages/mobile/src/nativeHost.ts", "ui/packages/mobile/src/index.ts", "ui/packages/mobile/src/__tests__/nativeHost.test.tsx", "ui/packages/fields/package.json", "pnpm-lock.yaml", "ui/packages/fields/src/widgets/ScanFieldButton.tsx (new, internal)", "ui/packages/fields/src/widgets/ScanFieldButton.shellScan.test.tsx (new)", "ui/packages/fields/src/widgets/TextField.tsx", "ui/packages/fields/src/widgets/LookupField.tsx", "ui/packages/fields/src/widgets/PeoplePicker.tsx (initialSearch only)", "ui/packages/fields/src/widgets/useFieldTranslation.ts (FIELD_DEFAULTS)", "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts", ".changeset/29378-shell-scan-fields.md" ], "public_surface_added": [ "@object-ui/mobile: NativeHostMethod gains 'scan'", "@object-ui/mobile: ObjectStackNative.scan?(opts) (optional member)", "@object-ui/mobile: type NativeScanOptions { formats?: string[] }", "@object-ui/mobile: nativeScan(opts?) resolving NativeCallResult of string or null", "@object-ui/fields: PeoplePicker prop initialSearch?: string", "@object-ui/i18n: fields.scan.button, fields.scan.failed (all ten locales)", "@object-ui/fields package.json: dependency @object-ui/mobile workspace:*" ], "line_budget": "22 files changed, 551 insertions, 6 deletions vs merge base (lockfile 3 lines); under the 3000-line class (c)", "tests": "head bf41e3ae: new ScanFieldButton.shellScan.test.tsx 13/13; mobile nativeHost.test.tsx 46/46. Packages: vitest packages/mobile/ packages/i18n/ = 104 files passed, 1567 tests passed 13 skipped; packages/fields/ = 294 passed 1 skipped, 4426 tests passed 7 skipped (lock VERDICT command-exit 0, 694 s). turbo type-check fields+mobile+i18n: 50/50 tasks, VERDICT command-exit 0. After merging main at c6569d87 (main touched none of fields/mobile/i18n/lockfile): the two scan test files 2 files, 59 tests passed. Ablation via scripts/ablation-replace.mjs on ScanFieldButton.tsx: anchor 'return host.has('scan') === true;' -> '(host.has('scan') || true) === true' landed (anchor 1->0, blob db4b436a -> 28645a70), exactly the two older-shell pins red (2 failed, 11 passed), restored blob == HEAD, git diff HEAD empty. Source-resolved through the ui vitest alias, no dist involved. Phone screenshots (390 px, stub bridge, temporary Console Vite entry, deleted): plain browser 0 buttons, shell 2 buttons, text value ASSET-0042, lookup search ASSET-0042 listing 1 row, failed toast 'Could not scan / Camera permission denied'.", "gates": "dispatch-gates --commands at cfa0fd12: 54 families; 53 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs full pnpm build; it scans root packages/ only). pnpm --dir ui exec vitest run scripts/: 169 files passed 2 skipped, exit 0. --ran reconciliation: 54 accounted, 53 run, 1 NOT-MEASURED, 0 UNRUN. check-sdui-manifest exit 0. ui check:new-line-citations 0 new. eslint on touched files: 0 errors after the fix commit bf41e3ae (warnings pre-existing plus one react-refresh warning on ScanFieldButton.tsx exporting a hook beside the component). PR-body gates on the sent body: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "deviations": [ "Surface widened before any edit by the seat's amendment 6581140256 (PeoplePicker.tsx initialSearch, useFieldTranslation.ts, locale files).", "Compact (inline grid cell) lookups get no scan button, like the single-select clear control; noted in Acceptance notes.", "A temporary Console Vite preview entry (ui/apps/console/scan-preview.html + src/scan-preview.tsx) was used for screenshots and deleted; git status clean.", "check:dual-build-cjs-loads NOT MEASURED locally (exit 3)." ], "mcp_calls": "4: issue_read x3 (card body, comments twice), get_session x1 (session id). No MCP write tool.", "api_writes": "2: fleet-write pr_create (PR 29493, read back 7730/7730 bytes identical); this post-stamped comment on issue 29378. git push is not a REST write.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 2, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Landed · 2026-11-02T17:06Z
PR #29493 merged at 2026-11-02T17:05Z as
ec2f5d5c.Fixes #29378closed this card;pm:dispatchedremoved. Delivered: inside the mobile shell, when the bridge announcesscan, editable single-line text fields and lookup fields show a scan button; a text field takes the decoded code throughonChange, a lookup opens its search with it, a closed scanner changes nothing and a failure shows the error toast with the shell's message.@object-ui/mobilegainsnativeScan/NativeScanOptions(optionalscan?on the bridge),PeoplePickeran optionalinitialSearch, andfields.scan.*keys in every locale (contract review PASS 6582783281). Nothing left on this card.