Skip to content

Console: offer a scan affordance on text and lookup fields when the mobile shell exposes scan() #29378

Closed

Description

@objectstack-fleet

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

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.

Duplicate check

2026-11-02, open and closed:

Query Hits Relevant
is:issue "has('scan')" 0 0
is:issue scan bridge console mobile field 4 0 (seat posts, a spec row, a registry)
is:issue barcode scan lookup field 0 0

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:ui · pm:queue · 2026-11-02T14:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/fields package.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/main 6fe99f86: 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#170 92a0a26).
    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.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

    Claim amended · 2026-11-02T15:32Z · on the os-dev's design note (at origin/main 5f7d554c), 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.
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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": []
    }
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

    Landed · 2026-11-02T17:06Z

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions