Page MenuHomePhabricator

advanced title and caption generation (!54)
Open, Needs TriagePublic

Description

Summary

Title and description (caption) are often written in similar ways — the title is frequently a short version of the description, and both are typically composed from values that are already known and filled in elsewhere in the table. Today the user types each cell from scratch, even when 80% of the content is mechanical recombination of existing fields.

This task proposes a variable-substitution system for title and description (and possibly other text fields), with savable, reusable templates built from those variables.

Round 1: design proposal (this round, no code)

Before any implementation, the implementer should produce a written proposal covering:

  • A complete list of available variables — every field/value in a row that could plausibly be substituted into a template. The user has named some examples (date in different formats, time, location names at different administrative levels, depicts labels, time of day like dawn/dusk/night) and explicitly asks the implementer to "propose all you can think of."
  • A template syntax — what does a variable reference look like in the cell? (e.g. {date:long}, {location:village}, {depicts:0} — propose).
  • Format modifiers — for variables like date, multiple format outputs need to be reachable (2026-05-10, May 10, 2026, 10 May 2026, etc.). Propose a modifier syntax.
  • Multilingual labels — depicts labels and location names have a language. The user explicitly flags this as a constraint: "keep in mind the language of labels from items." Propose how the language is selected (current UI language? per-template?).
  • Editing UX — when a cell is being edited, it should show variable names (raw template). When unfocused, it should show the substituted values so the user can trust their template works. Propose how this toggles visually.
  • Template persistence — templates should be savable for reuse. Where do they live (per-user, cross-device → wiki user-store Preferences.json)? How are they listed / picked / edited?
  • "Suggested" templates from past uploads — the user wants the system to surface templates inferred from previous completed uploads ("you've used {date:long} – {depicts:0} in {location:village} 12 times"). Propose how/whether this is detected.

Clarification questions to surface

After drafting the proposal, post it as a Phab comment and explicitly ask the maintainer (Daanvr) for clarification on the open design choices. The user has explicitly invited this: "ask me clarification questions".

Examples of questions to raise (add your own as the proposal solidifies):

  • Which fields should be substitutable? (Just title + description, or also author/source/categories?)
  • Variable syntax preference?
  • How aggressive should "suggested templates from history" be — explicit panel, or quietly surfaced as autocomplete?
  • Relationship to T425881 (wikitext-templates, currently in Reviewing) — they're conceptually adjacent (both deal with "templates"); confirm they're independent systems or that one wraps the other.

Out of scope (for the round-1 proposal)

  • Any code changes. This round is design/proposal only.
  • AI-generated suggestions for individual cells (separate concern; not what's being asked here).

Round 2 (after maintainer responds): implementation

Once the maintainer answers the clarification questions on Phab, the task is re-dispatched (or this same agent resumes via a new run) with locked-in design and proceeds through the normal implementer workflow.

Source

User feedback session, 2026-05-10. The user explicitly asked: "give me a list of variables that we could use" and "ask me clarification questions".

Event Timeline

Daanvr updated the task description. (Show Details)
Daanvr moved this task from To do to Doing on the Tool-upload-workbench board.

Round-1 design proposal: variable-substitution templates for title/description cells

Author: Claude design subagent
Repo grep depth: read-only walk through src/api/normalize.js, src/api/history.js, src/api/commons.js, src/api/user-store.js, src/api/publish.js, src/api/autocomplete.js, src/table.jsx, src/data.js, plus the in-flight feat/T425881-wikitext-templates branch (read at HEAD 35ec5af).

Scope reminder: this proposal is for per-cell variable substitution in title / description (and optionally other free-text cells). It is not the wikitext template picker T425881 added — see "Disambiguation from T425881" near the bottom.


1. Disambiguation from T425881 (wikitext-templates)

T425881 introduced src/wikitext-templates.js and uses placeholders of the form {{field:KEY}} inside the whole-page wikitext body posted at publish time. The substitution there happens once, during publish, server-side data flow:

T425881 (wikitext-templates)T425887 (this task)
---
Lives where?The whole Commons page body.Inside a single cell value (title, caption, etc.).
Template scopeOne per workbench, per user (Information / Artwork / Photograph / Book / Custom).Many per cell — user can save dozens of "title templates" and apply different ones to different rows.
Substitution momentAt publish (buildWikitext).Live, every render — the cell shows resolved values when unfocused, raw template when editing.
Placeholder syntax{{field:KEY}} (Mustache-with-prefix). Reuses {{ to harmonise with wikitext doubles.Proposed below — needs to be distinct from {{field:…}} so a custom wikitext template that contains a literal {date:long} etc. doesn't get pre-expanded by the cell-level renderer.
Format modifiersNone — only raw value substitution.Required ({date:long}, {depicts:0}, etc.) — much of the value is in formatting.

The two systems are complementary: a cell-level template substitutes {date:long}"May 10, 2026" in the Title cell. The Title cell value ("May 10, 2026 — Vrijthof at dawn") is then itself substituted into the wikitext via T425881's {{field:title}} placeholder. Two passes, two namespaces.

Recommendation: keep the namespaces lexically distinct so users (and reviewers reading wikitext on Commons) can tell which is which.


2. Variables — the complete catalogue

Drawn from the canonical row shape produced by normalizeStashItem and normalizeHistoryDetailedItem (src/api/normalize.js) and from SAMPLE_UPLOADS (src/data.js). Every plausibly-substitutable field is listed here, including ones that don't yet exist on the row but could be derived.

2.1 File metadata (always present, no derivation)

VariableFieldTypeExample valueNotes
-----
{filename}item.filenamestring"Maastricht_Vrijthof_dawn.jpg"Original-uploaded filename, with extension.
{filename:base}derivedstring"Maastricht_Vrijthof_dawn"Filename without the extension.
{filename:ext}derivedstring"jpg"Extension only, no dot.
{title}item.titlestring"Vrijthof Maastricht at dawn"The user-set title. Risky — a self-referential template in the title cell loops; we must short-circuit.
{description}item.descriptionstring"View northwards from the marl quarry plateau"Same self-reference caveat.
{author}item.authorstring"Joris van der Berg"Free-text. After T425874 lands, may be [[User:Foo|Foo]] — preserve that wikitext form? See open Q.
{license}item.licensestring"CC-BY-SA-4.0"Internal id. {license:label} could yield "CC BY-SA 4.0".
{source}item.sourcestring"Own work"
{bytes}item.bytesint4812344{bytes:human}"4.8 MB".
{width}, {height}item.width/.heightint4032, 2688{dimensions} derived → "4032×2688". {megapixels} derived → "10.8 MP".
{mime}item.mimestring"image/jpeg"{mime:short}"jpeg".
{sha1}item.sha1hex"a1b2c3..."Probably useless in user-facing text; include for completeness.
{me}from oauth profilestring"Daanvr"The current user's username. Same string T425874 will use.

2.2 EXIF / camera (often missing — degrade gracefully)

VariableFieldTypeExampleNotes
-----
{camera}item.camerastring"Sony α7 IV"Make + " " + Model.
{camera:make}derivedstring"Sony"Split on first space.
{camera:model}derivedstring"α7 IV"Remainder.
{lens}item.lensstring"FE 24-70mm f/2.8 GM"
{iso}item.isoint200
{aperture}item.aperturestring"f/8"Pre-formatted.
{shutter}item.shutterstring"1/60"Pre-formatted.
{focal}item.focalstring"35mm"Pre-formatted.
{exposure}derivedstring"f/8 · 1/60s · ISO 200"Convenience composite.

2.3 Date & time (the format-modifier playground)

The underlying source is item.dateTaken (ISO-8601 from EXIF DateTimeOriginal, fallback DateTime). All of the variables below operate on that one field.

VariableOutputExample
---
{date}default = iso"2026-04-29"
{date:iso}YYYY-MM-DD"2026-04-29"
{date:long}localised long"April 29, 2026" (en-US), "29. April 2026" (de)
{date:short}localised short"Apr 29, 2026"
{date:dmy}day-month-year"29 April 2026"
{date:ymd}year-month-day"2026 April 29"
{date:year}YYYY"2026"
{date:month}MM"04"
{date:month-name}localised"April"
{date:month-short}localised"Apr"
{date:day}DD"29"
{date:weekday}localised"Wednesday"
{date:weekday-short}localised"Wed"
{date:quarter}Q1–Q4"Q2"
{date:season}season name"spring" (depends on hemisphere — see Q below)
{time}default HH:mm"05:42"
{time:hm}HH:mm"05:42"
{time:hms}HH:mm:ss"05:42:00"
{time:12h}12h with am/pm"5:42 am"
{time:hour}hour, 24h"5"
{time:tod}time-of-day word"dawn" (see §2.4)
{datetime}combined ISO"2026-04-29 05:42"
{uploaded}item.uploadedAtsame modifiers as date apply
{published}item.publishedAtonly present on history items
{expires}item.expiresAtstash items only

Locale issue: Intl.DateTimeFormat is a free standard library — no extra dependency. The locale comes from the user's MediaWiki UI language (open Q below) with en as the fallback.

2.4 Time of day (computed from EXIF + GPS)

The user's spec mentions dawn / morning / noon / afternoon / dusk / evening / night. Two options:

Option A: Solar (correct, requires a library)

A real "dawn / dusk" classification needs the sun's altitude, which depends on lat/lng + UTC time + date. The classic JS lib is SunCalc — ~3 KB minified, MIT licensed, zero deps. It returns sunrise, sunriseEnd, goldenHourEnd, solarNoon, goldenHour, sunsetStart, sunset, dusk, nauticalDusk, night, nadir, nightEnd, nauticalDawn, dawn etc. as Date objects. We map the EXIF-time into one of those buckets:

night         → "night"
nightEnd → dawn → "dawn"
dawn → sunrise         → "dawn"
sunrise → goldenHourEnd→ "morning"
goldenHourEnd → solarNoon - 2h → "morning"
solarNoon ± 2h         → "noon"
… → goldenHour         → "afternoon"
goldenHour → sunset    → "golden hour"
sunset → dusk          → "dusk"
dusk → night           → "evening"

Granularity tunable: {time:tod} returns broad bucket (morning); {time:tod:fine} returns the finer one (golden hour).

Cost: +3 KB on the bundle (the workbench is ~600 KB minified gzipped today, so this is <0.5%).

Option B: Heuristic by clock (fallback / no GPS)

When GPS is missing, fall back to a clock-only heuristic:

Hour (local time)Bucket
--
0–4night
5–7dawn
7–11morning
11–14noon
14–17afternoon
17–19dusk
19–22evening
22–24night

Recommendation: Ship A + B together. If cameraLocation is present → solar. Else clock-heuristic. The variable always resolves to *something* so templates don't break.

2.5 Location (the Wikidata & granularity story)

We have three location sources on each row:

  • cameraLocation{lat, lon}, from EXIF GPS.
  • objectLocation{lat, lon}, user-set (where the depicted thing is).
  • locationOfCreation{qid, label}, a Wikidata item (P1071).

Granularity beyond the raw lat/lon needs reverse-geocoding. Three plausible backends:

  • Wikidata SPARQL on the QID (locationOfCreation) — when the user has set the QID, a SPARQL query can climb the P131 (located in admin entity) chain to fetch city → region → country. ~1 request per row, cacheable.
  • Wikidata Coordinate Search (wbgetentities + wbsearchentities + wd:nearby) — given lat/lon, find the nearest admin entity. Slower and lossier.
  • Nominatim (OpenStreetMap) — best free reverse geocoder for arbitrary lat/lon. Has a 1-req/sec limit and requires a contact email in User-Agent — already covered by Api-User-Agent. The OSM tile use in LocationEditor (table.jsx) already shows we ship OSM imagery; reverse-geocode would be a small addition.

Proposed variables (granularity from finest to coarsest, all multilingual):

VariableSourceExample
---
{location}best available label"Vrijthof, Maastricht"
{location:lat}numeric"50.8489"
{location:lon}numeric"5.6886"
{location:coords}"lat, lon""50.8489, 5.6886"
{location:hamlet}reverse-geocode"Vrijthof"
{location:village}reverse-geocode"Sint Pieter"
{location:town}reverse-geocoden/a
{location:city}reverse-geocode"Maastricht"
{location:district}reverse-geocode"Limburg"
{location:region}reverse-geocode"Limburg" (NUTS level varies)
{location:country}reverse-geocode"Netherlands"
{location:country-code}iso"NL"
{location:continent}reverse-geocode"Europe"
{location:qid}from locationOfCreation"Q1010"
{location:wikidata}label from QID"Maastricht"

Variants: {camera-location:…}, {object-location:…} to disambiguate when a row has both (e.g. a photo of Maastricht's main square taken from a hotel balcony in Maastricht — same place; or a museum photograph of an object far from the museum — different).

Default selection: when the user just writes {location}, we resolve to: objectLocationlocationOfCreationcameraLocationcoords, in that order. (Same hierarchy buildSdcClaims already uses for P625.)

2.6 Depicts / Wikidata items

item.depicts is [{qid, label}, …] — already labelled when fetched via the SDC bootstrap, but the labels are language-tagged.

VariableOutput
--
{depicts}comma-joined labels in current language: "Vrijthof, Maastricht"
{depicts:0}, {depicts:1}, …nth label (0-indexed). {depicts:0}"Vrijthof". Empty string if past end.
{depicts:0:qid}, …nth Q-id
{depicts:list}bullet list
{depicts:and}"Vrijthof and Maastricht" (Oxford comma if 3+)
{depicts:count}integer count

Multilingual: the label string used here is the one resolved at template-render time in the user's chosen language (open Q below). If the row's depicts only carries the QID (e.g. cached without label), we lazily fetch the label from Wikidata via the existing fetchWikidataEntity bridge, cache it for 5 min via apiCache, and re-render when it lands.

2.7 Categories

Categories are plain strings (Commons category names). Limited utility in templates but listed for completeness.

VariableOutput
--
{categories}comma-joined: "Sint-Pietersberg, Panoramas of Limburg (Netherlands)"
{categories:0}, {categories:1}, …nth
{categories:count}int

2.8 Derived / computed (the surprise candidates)

The user invited "propose all you can think of." Some of these may be silly; flagged in the open-Qs section so you can prune.

VariableOutput
--
{orientation}"landscape" / "portrait" / "square" (from width/height)
{aspect}"3:2" / "16:9" / "4:3" etc. (closest common ratio)
{format}"raw" / "jpeg" / "png" / "tiff" / "video" / "audio" (from mime)
{is-archival}"yes" / "" (true when filename matches scan_/archive_ heuristics)
{counter}row's index in the current upload batch (1-based). Useful for "Photo 1 of 24".
{counter:total}total in batch.
{counter:padded}"01", "02" zero-padded.

2.9 Per-row helpers (string operations)

These are *transforms* of any other variable, applied as a chained modifier:

ModifierEffect
--
{var:upper}uppercase
{var:lower}lowercase
{var:title}title-case
{var:trim}strip whitespace
{var:slugify}filename-safe slug
{var:fallback:value}use literal "value" if var resolves to empty
{var:max:60}clip to N chars

Modifiers can chain: {date:long:upper}"APRIL 29, 2026".


3. Template syntax — proposal

Recommended: {var} and {var:modifier} and {var:0} (positional) and {var:0:qid} (sub-attribute).

Why not {{var}}

Because the wikitext output already swims in {{…}} (Commons templates: {{Information}}, {{cc-by-sa-4.0}}, {{en|1=…}}, {{Creator:…}}). A user who types {{date}} in a title cell is much more likely to mean "the literal string {{date}}" — perhaps a Commons template name they're referencing. Single-brace is unambiguous.

Why not ${var} (JS template literal)

Familiar to programmers, but the dollar sign also has weight in wiki land (gettext-style $1). Single-brace is cleaner.

Escaping
  • \{ and \} are literal braces.
  • Anything inside {} that doesn't match a known variable resolves to either:
    • the raw template text, with a visual flag (greyed out, italics) so the user sees "this didn't resolve". OR
    • empty string + a warning underline — same visual treatment as a validation error.

I lean toward raw text + greyed. It makes typos visible without breaking the rendered output unrecoverably.

Reserved words
  • No keywords yet. Future expansion: {if:date}…{end} for conditional blocks. Out of scope for v1.
Self-reference loop

If a Title cell contains {title}, that's an infinite loop. Detect at render: if the variable is the same field as the cell, resolve to empty string + warning.


4. Edit-vs-display UX

Today's Cell (src/table.jsx:935) renders different markup based on isEditing. For our case:

Display mode (unfocused / not editing)
  • The cell shows the rendered value: {date:long} – {depicts:0}"April 29, 2026 – Vrijthof".
  • A subtle indicator (e.g. small chevron icon, or a faint underline color) signals "this cell is template-driven, not literal." Clicking the indicator opens a tiny popover that says "Template: {date:long} – {depicts:0}. Click to edit."
  • Unresolved variables render as {date:long} literally with text-decoration: underline dotted; color: var(--color-warn). Hover tooltip explains why (e.g. "no dateTaken value on this row").
Edit mode (focused)
  • The cell flips to the editor (TextEditor for description; TitleEditor for title).
  • The editor's input value is the raw template ({date:long} – {depicts:0}), not the rendered value.
  • Below the input, a live preview strip shows what the substituted value will look like once committed (greyed text, smaller font). Resembles the URL-bar autocomplete in browsers.
  • Variable autocomplete: typing { opens a dropdown of all available variables, sorted by relevance (variables for which this row has a value sort first). Picker behavior reuses the existing AutocompletePop component.
Storage model

The cell stores the raw template string, not the rendered value. So:

  • item.title = "{date:long} – {depicts:0}" (raw)
  • The renderer (a pure function) produces the display string from item + template.
  • When the user wants to "freeze" the template into a literal string (e.g. before publish, or to break the binding), a small "Convert to literal" action in the cell overflow menu does it once.
Storage location

This raw template lives in the same item.title / item.description field that the editor already writes to via setDraft. Drafts get saved to wiki via the user-store's debounced 3 s flush — no schema change needed for per-row template storage.

Bulk apply

A new "Apply template to selection" toolbar action (only enabled when ≥ 1 row is selected). Picks a template from the library (see §6) and writes it to every selected row's title (or description, depending on which column the user invokes it from). Doesn't render-and-freeze: each row keeps the raw template, so changing one row's date updates that row's rendered title.


5. Multilingual labels — language selection

Three places where language matters:

  • Wikidata labels ({depicts:0}, {location:wikidata}) — Wikidata stores labels per language (labels.en.value, labels.nl.value, …).
  • Reverse-geocoded names — Nominatim returns names per language via accept-language header.
  • Intl.DateTimeFormat — uses a BCP-47 locale.
Proposed selection cascade
  1. The cell-level template's pinned language, if set (per-template setting in the library — see §6).
  2. The workbench's currently-active language, if T425864's "Add language" placeholder lands and lets the user toggle.
  3. The user's MediaWiki UI language, fetched at bootstrap from action=query&meta=userinfo&uiprop=options (specifically the language user option). This is the language they've selected on Commons itself.
  4. Browser default (navigator.language).
  5. Hard fallback: en.

Level 3 is the right default — it's "the language this user reads Commons in." Level 1 lets a power user say "this template is for German captions, always render in de regardless of UI."

Per-template pinning vs per-cell

The user might want one cell rendered EN and another DE. Options:

  • Per-template language — every template has a default language; the rendered output uses it. A user maintains "English title", "Dutch title" templates, applies each as needed. Simpler.
  • Per-cell language tag — the cell carries {lang:nl, template:'…'}. More flexible, more state.

Recommendation: start with per-template. The per-cell layer can be added later when T425864 unlocks per-column language selection on the caption column.

Label fetching

Today seedFromHistory (autocomplete.js) seeds depicts QIDs into a cache, and fetchWikidataEntity(qid) fetches one. The current call sites pass language='en'. To support multilingual we extend the cache shape to Map<qid, Map<lang, {label, desc}>> and add the language to the cache key. Cost: modest — one extra wbgetentities call per language per QID, cached 5 min via apiCache.


6. Template persistence + library UI

Storage shape (in Preferences.json, namespaced)
json
{
  "schemaVersion": 3,
  "cellTemplates": {
    "title": [
      {
        "id": "abc123",
        "name": "Date + first depicts",
        "body": "{date:long} – {depicts:0}",
        "lang": null,            // null = inherit from cascade; "nl" = pin
        "createdAt": "2026-05-10T14:00:00Z",
        "lastUsedAt": "2026-05-10T18:00:00Z",
        "useCount": 12
      }
    ],
    "description": [...],
    "caption": [...]
  }
}

The shape is grouped by field key so each cell's picker only shows templates relevant to it. useCount and lastUsedAt drive sort order in the picker, and feed the "suggested from past uploads" engine (§7).

Per-user, cross-device — fits perfectly in Preferences.json. The 3 s debounced save already exists. No risk of sha1Index-style derived-data ballooning: templates are explicitly user-authored.

Schema migration

Bump schemaVersion to 3, add cellTemplates: {} if missing on load. No-op for existing users.

Library UI placement

T425881 already added a "Templates and columns" tabbed modal (renaming the old "Columns" button). This task adds a third tabCell templates — to that same modal. Three tabs:

  1. Wikitext templates (T425881)
  2. Cell templates (new — this task)
  3. Columns (existing)

In the Cell templates tab:

  • Group selector at the top (Title / Description / Caption / etc.) — only fields where templates are supported.
  • List of templates for the selected group, with name + body + last-used.
  • "New template" button → small editor: name, body (with variable autocomplete), language pin (default: inherit), preview against any selected row.
  • Per-template menu: rename, delete, duplicate.
  • Drag to reorder for picker order.
Per-cell picker (the actual usage point)

When the user opens a Title cell editor, the editor sees a small "Templates" button (or a dropdown) sitting at the right edge of the input. Clicking it lists this user's title templates with previews against the current row. Click → fills the cell with that template body.


7. "Suggested templates from past uploads"

The user wants the system to surface templates inferred from their published history ("you've used {date:long} – {depicts:0} in {location:village} 12 times").

Two rough approaches, listed cheapest-to-implement first:

Approach A: Save-on-publish (explicit, simplest)

After a successful publish, if the user wrote the title without using a template, offer a one-line modal: "Save this title pattern as a template?" — the system tries to back-derive a template from the literal title by spotting substrings that match known field values:

  • literal "April 29, 2026" in the title → {date:long} slot.
  • literal "Vrijthof" matches depicts[0].label{depicts:0} slot.
  • literal "Maastricht" matches location:city{location:city} slot.

The user sees the proposed template body ("{date:long} – {depicts:0} in {location:city}") and can accept, edit, or dismiss.

Pros: no API cost, no bulk history scanning, transparent to the user, opt-in. Maps directly to the "save reusable template" mental model.

Cons: doesn't surface patterns the user *implicitly* repeated across many uploads — they'd need to opt in once per pattern.

Approach B: Mine the cached history (more magical)

The workbench already caches the latest 50 published items in metadata.history.items (user-store.js:592). For each item we have title, dateTaken, depicts, location-ish state. We back-derive a template per item (same algorithm as A), then group identical bodies and surface the most-frequent ones in a "Suggestions" section of the Cell templates tab.

Pros: magical: opens the modal, sees "12 of your last 50 titles fit this pattern: {date:long} – {depicts:0} in {location:city}. Save as template?" — one click.

Cons: more complex; back-derivation is heuristic and will produce false positives ("April" appears literally in a title for an unrelated reason → wrong template). False positives are recoverable (user dismisses), so not catastrophic, but UX-sensitive.

Recommendation

Ship A in v1. It's simple, low-risk, and demonstrates the value. Add B in a later round once we've seen real patterns in cached history.


8. Open design choices (the question list)

In rough priority order — answer in the comment thread and I'll resume.

  • (Q1) Variable syntax: single-brace {var} (my recommendation) vs {{var}} vs ${var}? I'm 80% confident single-brace is right because of wikitext-double collisions, but you have stronger taste here.
  • (Q2) Time-of-day: bundle SunCalc (~3 KB, +1 dep, accurate) or clock-only heuristic (no dep, sloppy near twilight)? I lean SunCalc.
  • (Q3) "Suggested from history" scope: A (save-on-publish, opt-in) or B (mine cached 50, surface frequent patterns) or A+B? I lean A first.
  • (Q4) Multilingual default: pull from MediaWiki's userinfo.language user option, with per-template override? Or do you want a separate workbench-wide language toggle in the topbar?
  • (Q5) Reverse-geocoding for {location:city} etc.: Nominatim (good for arbitrary lat/lon) vs Wikidata SPARQL P131 chain (only works when locationOfCreation is set)? I lean both — Wikidata-first when QID is present (one cheap API call), Nominatim fallback. Confirm Nominatim use is OK from a Wikimedia-tool-policy perspective.
  • (Q6) Which fields are template-able? I'd cap v1 at title, description, caption (all free-text user-authored cells). Possibly categories too — a template like Photographs by {me} taken in {date:year} is plausible there. Author is interesting because of T425874 (Me (Username) quick-select) — see Q7. Source is rarely free text. Out for v1: license, depicts, location (structured types, not free-text).
  • (Q7) Coupling with T425874 (Me): should {me} always render as the wikitext form [[User:Foo|Foo]], or as the bare username "Foo"? Probably bare in cell display (a title cell shouldn't contain wikitext markup), but pre-publish substitution into the wikitext author= field should re-wrap. Confirm.
  • (Q8) Self-reference loop: confirm the policy "if {title} appears in a template applied to the title cell, render as empty string + warning."
  • (Q9) Extension point: template variables for custom-properties columns (T425864)? The workbench supports user-added custom property columns. Should those become substitutable as {prop:P12345} automatically, or do we wait until users ask?
  • (Q10) Storage cost / wiki-edit volume: any concerns about Cell templates living in Preferences.json? The expected size is small (a typical user has 5–20 templates × ~100 bytes each = <2 KB) but I want to confirm we're not heading toward a sha1Index-style problem if a power user saves 500 templates.
  • (Q11) Date-modifier season-hemisphere: should {date:season} use the camera's lat/lon (so a photo taken in Sydney in April resolves to "autumn") or always northern-hemisphere? Probably first-with-fallback.
  • (Q12) "Convert to literal" workflow: when a user wants to break the template binding and freeze the rendered value, where does that action live? Cell overflow menu? Edit-mode escape? I lean: a small button in the edit-mode preview strip ("Convert to literal text").

9. Couplings to other in-flight work

  • T425864 (per-column dropdown / Add language placeholder, in Reviewing) — when "Add language" lands, it'll surface a per-column language choice for caption. This task should consume that signal as the language source for cell-template rendering on the caption column.
  • T425878 (Caption rename + validator, in Reviewing) — captions get a length cap (~250 chars). Cell templates rendered into a caption cell must respect the cap; if the rendered output exceeds it, the validation error path should fire same as for literal text.
  • T425880 (title validation, in Reviewing) — title char restrictions + uniqueness check. The validator runs on the rendered title, not the raw template. A template like {depicts:0} rendered to "Vrijthof" triggers the uniqueness check on "Vrijthof".
  • T425874 (Me-author quick-select, in Reviewing) — see Q7; {me} overlaps with the structured "uploader-as-author" wikitext form.
  • T425879 (wikitext preview, in Reviewing) — the per-row wikitext preview should show the resolved title/description (the rendered values), which is what gets posted. The raw template never touches Commons; it's a workbench-only construct. This is important to surface in any preview screen.
  • T425881 (wikitext templates, in Reviewing) — see §1. Distinct namespaces, two-pass substitution.
  • T425884 (bulk discard duplicates) — no coupling.
  • T425883 (hidden files) — no coupling.

10. Suggested implementation breakdown

A reasonable split into MR-sized chunks. Each MR independently mergable + ships value.

MR-1: Variable resolver + 5 core variables + display toggle
  • New module src/cell-templates.js: resolveCellTemplate(item, body, context) returning the rendered string + { unresolved: [...] }. Pure function, easy to test by hand.
  • Variables shipped: {filename}, {filename:base}, {date:iso}, {date:long}, {depicts:0}. Five — enough to demo end-to-end.
  • Cell (src/table.jsx) displays the rendered value when not editing, raw template when editing. Add the small chevron indicator. No template library yet; users hand-type the body.
  • Manual test path: type {filename:base} into a title cell, blur, see the basename. Re-edit, see the raw template. Publish, see the rendered value go to Commons.
MR-2: Format modifiers + dates + {me}
  • Add the date modifier matrix (§2.3). Intl.DateTimeFormat plumbing.
  • Add {me}, plumbed from oauth profile through App props.
  • Variable autocomplete (typing { opens picker).
  • Edit-mode preview strip (live rendered preview below input).
MR-3: Cell template library UI
  • Schema bump for Preferences.json, cellTemplates field added.
  • Third tab in the Templates-and-columns modal (Cell templates).
  • New / edit / delete / reorder.
  • Per-cell picker dropdown in the editor.
  • Bulk-apply toolbar action.
MR-4: Multilingual + Wikidata label resolution + locations
  • Multilingual label fetching (extend fetchWikidataEntity cache to per-language).
  • {depicts:0} honors the chosen language.
  • {location:wikidata} (Wikidata SPARQL P131 chain).
  • {location:city} / {country} / etc. via Nominatim fallback (only fired on demand, cached aggressively).
  • Pinned language per template.
MR-5: Time-of-day + derived variables
  • Bundle SunCalc + write the bucket mapper.
  • Add the derived variables ({orientation}, {aspect}, {format}, etc.).
MR-6: Suggested-from-history (Approach A)
  • Post-publish "save this title pattern?" hook.
  • Back-derivation algorithm.
  • If we want B later, that's MR-7.

If maintainer wants a faster minimum: MR-1 + MR-3 alone deliver enough value to publish and gather feedback. MR-2 / MR-4 / MR-5 are independent enhancements that can land in any order afterward.


Ready for your feedback on the open Qs (especially Q1, Q2, Q3, Q4, Q5). Once those are settled, I (or the next implementer subagent) will move to round 2 and start with MR-1.

@Daanvr Round-1 design proposal posted in the previous comment. Awaiting your feedback on the 12 open design choices listed under section 8 — most-blocking are Q1 (variable syntax single-brace vs double), Q2 (SunCalc dep for time-of-day), Q3 (suggested-from-history scope: opt-in save-on-publish vs auto-mine cached history), Q4 (multilingual default source), and Q5 (reverse-geocoding backend choice + Wikimedia-policy-OK on Nominatim). Once those are settled, I'll proceed to round-2 implementation starting with MR-1 (variable resolver + 5 core variables + display toggle).

Correction — Wikidata property links

The earlier design proposal comment refers to several Wikidata properties as bare P<n>. Those got auto-linked by Phab to paste objects with the same numeric IDs (P<n> is paste shorthand in Phab markup), pulling unrelated objects into "Mentioned Here". The intended references were Wikidata properties — corrected links below:

P12345 in the proposal was a generic placeholder for syntax demonstration ({prop:P12345}), not a real property — should be read as P<id>.

(The original comment can't be edited via the Phab API — only via web UI. The bogus "Mentioned Here" entries from the original comment will linger unless the original is edited manually.)

this task is a bit oudate. let me update you: description has dissapeard. it is caption now, as it should be.
related task: title variable in caption cels.

the design i am looking for is that the user can write text like usal but they can add "values" from elsewhere as chips inbetween the text. these chips might have some options like how to structure the date and time and what language to use. these options are available by clikng on the chip. chip suggestions can be displayed under the text. all values elsewhere in the row should be available as they might be relevant. for wikidata items and categories their labale is relevant. the categorie prefix should be removed. for chips that are several words, when clicking on it the user should be able to click words to toggle them. untoggled words should appear as strikethrough. A chip should display as the text it will be. helping the user to understand what the totality of the text will be and help the system to gard the string length limit. when clicking the chip to edit the text, the suer should se the current version, the possible variations in the templates of the values should be listed below (also as the result not the template design) and above should be what variable is in the chip (column name, Qid Label, category, ...) language should also be abel to be selected for example of wikidata items. cordinaes should show their labels from different levels, liek to the most local authority to the more global authority. the sort of template created should be saved in the usernamespace. the user should be able to acces the last x used and saved ones. saving and loading them should be in a modal to keep it seperate form editing them.

if there is contradicotry info in this comment and in earlyer descriptions of comments on this task, the latest is the one that should be followed.

Daanvr renamed this task from advanced title and description generation to advanced title and caption generation.May 15 2026, 4:05 PM
Daanvr moved this task from Backlog to Doing on the Tool-upload-workbench board.
Daanvr moved this task from Doing to To do on the Tool-upload-workbench board.

dont use the { } use the chips

Daanvr renamed this task from advanced title and caption generation to advanced title and caption generation (!54).May 15 2026, 4:40 PM
Daanvr moved this task from Doing to Reviewing on the Tool-upload-workbench board.

MR-1 of multi-MR rollout. MR: https://gitlab.wikimedia.org/daanvr/upload-workbench/-/merge_requests/54
Preview: https://upload-workbench.toolforge.org/mr-54/

Scope: chip-based editor for the Title cell only. Variables shipped: filename (basename/full/ext), date taken (7 formats), depicts (per QID, multilingual), location-of-creation (per QID, multilingual), categories (Category: prefix stripped per the brief), author. Chip popover gives format options (date) AS rendered values (not template syntax), per-chip language picker for Wikidata items, per-word toggle (multi-word values, struck-through when off, omitted in publish), warn-but-allow length-limit guard at 250 chars (Wikibase SDC label cap).

Deferred to MR-2: Caption cell support, save/load template library (separate modal), recent-templates panel.

Deferred pending policy clarification: coordinates → multi-level admin labels via Wikidata SPARQL P131 chain + Nominatim fallback (open Q5 from Round-1 was never explicitly answered re: Nominatim use from a Wikimedia-tool-policy perspective — single-label locationOfCreation chip IS shipped in MR-1).

Per the maintainer's brief ('dont use the { } use the chips'), no { } template syntax is exposed anywhere in the user-facing surface — segments are an implementation detail of the storage/resolver, never shown to the user. Round-1 design document is superseded by this implementation per https://phabricator.wikimedia.org/T425887#11926139.

no +chip in Captions
when cllicking +chip the suggestion apear under the typing suggestions and such.
the text in the inline chip does reflect the final text expet for the missing spaces.
once a chip is added the text field is not editable as text anymore. my cursor won’t move to before the chips ot add a space or things liek that. only move areoutn in th text after the last chip. clickign on the text befor the chip wil result in unfocus of the cell.
the smal icon with a letter at the start ot the chip is not nessesary. it is not helpful it should be removed.
the resultign text form several chips is sticked together with no spaces or other seperator. that should be a space by default but the suer sould simply be abel to go there with the arrow keys or the mouse cursor to ad something different in between the chips including custom text.

Addressed feedback on !54:

  1. Cursor placement before / between chips: rebuilt the editor as a multi-input layout — one auto-sized <input> per text run between chips. Cursor now lands wherever you click; ArrowLeft at offset 0 jumps to the previous text input (skipping the chip in between), ArrowRight at end jumps to the next; Backspace at offset 0 deletes the chip immediately before the input and merges the surrounding text. Mouse clicks before / between chips no longer blur the cell because focus stays inside the editor's container.
  1. Default-space separator between chips: inserting a chip now auto-adds a single ASCII space on each side (only when the surrounding literal text doesn't already have whitespace there). When a chip is added at the END of existing content, a trailing space is seeded into the right-side input so the user can keep typing without a missing-separator. Deleting a chip collapses paired boundary whitespace back to a single space, so you don't get a "two spaces in a row" validator error after unwinding.
  1. +chip suggestions appear under the typing suggestions: ChipPicker is no longer an absolutely-positioned popover — it renders in-flow at the bottom of the editor, AFTER the validation status + recently-used-titles autocomplete. Both lists now share the same below-editor area.
  1. Letter icon at start of chip removed: the per-kind F / D / Q / L / C / A badge is gone. The kindEmoji() function and the chip-pill__icon JSX are both removed; the CSS class is kept as display: none defensively in case a stale chip element reaches it during HMR/migration.
  1. No +chip in Captions: confirmed — Captions use CaptionEditor, which is its own component without ChipEditor. The +chip button only renders inside ChipEditor, which is only wired into the Title cell. No code change needed for this point.

Verification:

  • npm run build clean (incl. undefined-identifier scanner).
  • Deployed bundle at /mr-54/ contains chip-editor__seg (multi-input wrapper) and chip-editor__mirror (auto-width measurement); contains 0 references to chip-pill__icon and kindEmoji (removed code).
  • Deployed CSS at /mr-54/ confirms .chip-picker no longer has position: absolute (now in-flow with margin-top: 4px), and .chip-pill__icon { display: none } is the only rule.
  • Could not exercise interactively from a curl: the multi-input cursor placement (ArrowLeft/ArrowRight jumping between segments, click-before-chip preserving focus, Backspace-at-offset-0 chip deletion, auto-space insertion when picking a chip mid-text) all need a live browser to validate end-to-end. Worth a quick spin in the preview to confirm the editing UX feels right.

New commit pushed to feat/T425887-chip-template-editor; preview redeployed at https://upload-workbench.toolforge.org/mr-54/.

Skipped during bulk-merge orchestration (5 sibling MRs landed today as v0.32.0–v0.36.0). The rebase of !54 onto current main produces ~500 lines of conflict in src/table.jsx against the auto-sequence work that landed in v0.33.0 (T425984): !54 replaces TitleEditor's state model (v{rendered, segments}) and the sequence-suggestion logic from T425984 is woven into the pre-chip v-based editor (placeholder gating, ownership-based suppression, etc.). Merging !54 as-is would silently drop the auto-sequence integration with the title editor — a regression of T425984.

Integration needs a maintainer-level design call: does the chip editor onAcceptSequenceSuggestion from the chip rendered string, or does the auto-sequence chip become its own chip type? Punting to maintainer.

Branch feat/T425887-chip-template-editor left untouched at 398a21e for direct review.

explaine the technical considerations in more generic termis insted of the specific functions ore code peaces since i am not nose deep in the codebut want to understand and make a good and deliberate decision that makes sense in a strategic cense fro the code.

@Daanvr Re your 06:30 ask — here is the same situation explained without code-specific references. The previous comment was right that a maintainer-level design call is needed; this one tries to lay out what the call is about so you can make it deliberately.

The situation in plain terms

There are two pieces of work that both reshape the same surface area of the app: the editor that opens when you click a Title cell.

(1) The auto-sequence feature (T425984, already merged as v0.33.0). When you type a title that would collide with another file — either another row in your current upload or a file you previously published on Commons under that exact name — a "sequence chip" appears under the input offering "Convert to Foo #". Accepting it rewrites your row (and every other colliding row) so each file ends up uniquely numbered. The editor knows about: which other rows it might collide with, whether the colliding file on Commons was uploaded by you (vs someone else, where the suggestion wouldn't help), and whether you're already in placeholder form (don't re-suggest).

(2) The chip-based editor (this task, T425887). The Title cell is no longer "one text box" — it's a row of literal text fragments and chip pills you can insert anywhere. Each chip is a typed reference to a value somewhere on the row (filename, date, depicts label, etc.) and renders as the resolved string. You see the total visible text at all times; the chip carries the recipe under the hood.

Both features rewrite essentially the same component — the Title cell editor — and !54 was branched before T425984 landed. So neither side knows about the other. Merging as-is would silently delete the auto-sequence behaviour from the Title editor. That's the regression the previous merger flagged.

What the integration call is actually about

The two features overlap in *one specific way*: they both want to add an inline visual "thing" sitting at or near the title input — a tappable button-like element that suggests a transformation of the current title.

The auto-sequence feature already calls its visual element a "chip" in the code and in the user-facing tooltip. The new chip editor, of course, introduces actual chip pills as a core concept. So you have two things both called "chip" in roughly the same UI region, but conceptually very different:

  • The auto-sequence chip is an *action button* — it appears conditionally, sits below the input, and clicking it rewrites the row. It's a one-shot affordance.
  • A value chip (this task) is *part of the title content itself* — it sits inline with the text, persists, and gets resolved to a string at render/publish.

The strategic question is: should these two stay as distinct UI affordances, or does the auto-sequence concept become "just another chip kind" inside the new editor?

The two paths

Path A — Keep auto-sequence as a separate affordance, layered into the new editor.

The Title cell becomes a chip-based editor (per the new design), AND it also surfaces the auto-sequence suggestion exactly as it does today — a tappable element below the input, shown when the rendered title would collide. The two systems live side-by-side: chips are the user's content, the sequence suggestion is a one-shot action.

Pros:

  • Faithful to both designs. Each feature behaves exactly as you and others have iterated it.
  • The auto-sequence chip carries a lot of UX work that's already been signed off — collision detection across rows, "your file vs someone else's" branching, the click-to-see-existing-numbers info popout. None of that has to be re-thought.
  • Clearer mental model for the user: chips inside the title = "content you composed"; chip below the title = "system suggestion".

Cons:

  • Two visually similar things called "chip" in adjacent UI. Risk of user confusion.
  • The two systems both want the area below the input (auto-sequence chip, suggestions panel, vocab autocomplete, +chip picker). Real-estate competition needs to be managed — but this is solvable; it's the same kind of vertical stacking that already exists below the input today.

Path B — Make auto-sequence a chip type inside the new editor.

When the system detects a collision, it offers (in the +chip picker, or as a one-click action) a special "Numbered sequence" chip that the user inserts into the title. Once inserted, that chip resolves to the integer at publish time. The auto-sequence becomes another kind of value reference, like "date" or "depicts".

Pros:

  • One coherent concept: everything in the title that isn't literal text is a chip.
  • No two-systems UI confusion.

Cons:

  • The auto-sequence is *bulk* by design — accepting it rewrites every colliding row, not just yours. A chip lives in one row's content. Bulk-apply would need new machinery to insert the same chip on N rows in one shot.
  • The user experience changes meaningfully: today, accepting the suggestion is one click and you see the result immediately ("Foo #"). As a chip-insert, the user has to pick the chip kind, position it, and trust that the visible "1" will become "2", "3"… at publish time across the colliding rows. More mental load for a workflow that's specifically supposed to be one-click.
  • Sequences and chips have different lifecycles. A chip is per-row content. A sequence number is decided at *publish* time by the resolver, not at edit time — the rest of the chip system is "compute now, show now". Mixing the two needs care.

Path C — Ship the chip editor without auto-sequence, accept the regression.

Smallest amount of integration work. The Title cell becomes the chip editor and loses the auto-sequence suggestion until a follow-up task re-adds it (probably as Path A in a smaller scope).

Pros:

  • Fastest path to landing !54.
  • Lets the chip editor get real-user feedback without the integration complications.

Cons:

  • It's a deliberate regression of v0.33.0 work, which only landed yesterday. The auto-sequence chip was your specific feedback iteration (the "ghost chip + tooltip + click-info popout" version is what shipped). Removing it days later is a bad signal.
  • You'd be making the user re-learn the title workflow twice in quick succession (auto-sequence chip arrives → goes away → comes back differently).

Recommendation

Path A. The two "chips" are different things, and trying to unify them now (Path B) trades a real UX win (one-click bulk sequence resolution) for a smaller naming-coherence win. The two systems have already converged on calling their visual element a "chip" because, well, that's what pill-shaped tappable UI is called everywhere — but they're conceptually distinct (one is content, one is a system suggestion). The user-visible labels can disambiguate ("Sequence" vs the value's name on the inserted chip).

Path A is also the lowest-risk merge. The auto-sequence wiring is mostly props threading and a conditional render — adding it to the new editor without re-deriving it costs roughly one merger session, not a redesign.

What I need from you

Just a pick: A, B, or C. If A, I'll port the auto-sequence wiring into the new editor and push a rebase. If B, we should probably scope a deeper redesign (the sequence chip needs to become a real chip kind with publish-time resolution, which is a real piece of work — not a merge fix). If C, I'll rebase clean, drop the regression, and we file a follow-up task for re-integration.

the answer is: the auto-sequence concept become "just another chip kind" inside the new editor.
So the use of the hashtag uh as the last character in um the string will still trigger it. Uh and um you can still add the the the numbering. You can also add it from um the uh From the list of suggestions from chips, but it should all work on the same um New editor with the chips uh system. So it should have the same code style and should have the same uh uh work the same thing. It's just a uh uh a bit of a special chip, I guess, because it has some different actions uh and will not directly resolve to Actually it should ac uh uh resolve to numbering. Um but it should clearly still indicate it's part of this um sequence scheme.

So feel free to rewrite uh or actually I would recommend you Integrates T425984 in this code then publish wilst overwriting the previous code by T425984

Picked up after maintainer's resume signal in comment 11927794 ("auto-sequence concept becomes 'just another chip kind' inside the new editor", "integrating T425984 code into this code, then publish whilst overwriting previous T425984 code").

Done in this push

Branch was 9 versions behind main (branched v0.28.0; main now v0.37.0). Merged origin/main into the branch and integrated T425984's auto-sequence logic into the chip-based TitleEditor.

Conflicts resolved:

  • src/api/user-store.js — DRAFT_FIELDS keeps both titleSegments (T425887) and descriptions (T426422 per-language captions).
  • src/app.css — concatenated both sides' new rules (chip-editor + topbar version chip from T426443 + info-modal accordion tweaks + sequence chip CSS from T425984).
  • src/table.jsx — six conflict regions in TitleEditor, fully integrated:
    • TitleEditor's value-driving state is the chip-editor's rendered (the chip-resolved string), not the raw <input> value. All validation, the Commons uniqueness check, sibling-collision lookup, own-Commons-collision detection, and the placeholder gating now run against the chip output.
    • Sequence-suggestion strip + info popout move into the chip editor's footerSlot, after the validation status row and before the recently-used-titles autocomplete.
    • Accepting the sequence suggestion collapses any chip segments into the literal <basename> # placeholder (single text-only segment, no chips); publish-time resolver in src/api/sequence.js is unchanged.
    • Enter-accepts-sequence shortcut dropped — chip editor's Enter commits the typed value as-is; sequence acceptance is now click-only (matches the directive that auto-sequence be "one explicit affordance among many chip-driven options" rather than a magic keyboard shortcut). The chip button's "Enter" hint label is removed.
  • CHANGELOG.md — auto-merged cleanly; added a new Changed entry to [Unreleased] explaining the auto-sequence integration.

Verification

For your review

The preview is at https://upload-workbench.toolforge.org/mr-54/. The integration sketches the simplest path through your directive ("resolve to numbering but clearly indicate it's part of sequence scheme"): the sequence chip surfaces in the footer once a collision is detected, click-to-accept rewrites to <basename> # (literal). A future iteration could replace that literal with a typeable # chip kind, but the publish-time resolver still expects the literal form, so it's the smallest change that lands the integration without re-touching sequence.js.

Updated MR description: https://gitlab.wikimedia.org/daanvr/upload-workbench/-/merge_requests/54

MR: https://gitlab.wikimedia.org/daanvr/upload-workbench/-/merge_requests/54
Preview: https://upload-workbench.toolforge.org/mr-54/

Uh some new new feedback is that the um file name suggestions are still overlapping the Suggestions for chips. So whenever the chips menu is activated, the file name suggestions should be disappearing. uh and I give you a screenshot about this to give you a bit more context. Then the um plus chip button at the end of the the the input field um should not say chip because that's the technical term but it should say variable That's logical for the user. Also the chip design is a bit buggy right now. has two like pill shaped uh borders inside of each other. Um this is incorrect and should be normalized. Keep in mind the other chips. Uh and also I would like to be able to reorder the chips um by drag and dropping. And I want there to be a white space in between each chip by default Such that they when you add new ones they don't touch each other.
And an other user experience annoyance is that you're Changes don't get saved whenever you unfocus on that cell. Meaning if you click outside of the cell, all changes disappear. That should not be the case, that is never the case in this table.
overlaping suggestions:

Screenshot 2026-05-16 at 11.25.21.png (1,190×954 px, 166 KB)

chip design and +chip button
Screenshot 2026-05-16 at 11.34.12.png (1,296×88 px, 29 KB)

Addressed all six points from your 09:34 comment in two new commits on !54 (72299f2 + 124229e — base 7a1d8ad preserved as you asked).

  1. Filename suggestions are hidden whenever the variable picker is open (TitleEditor tracks the chip-editor's picker state via a new onPickerStateChange callback fired from a useLayoutEffect so the two never overlap, even for one frame).
  2. "+ chip" → "+ variable" everywhere user-visible (button label, tooltip, aria-label, placeholder, popover Delete-button, empty-picker message). The technical term "chip" stays only in code-internal identifiers.
  3. Double pill border fixed by renaming the editable-chip outer wrapper class from .chip-pill to .chip-pill-wrap. Previously the outer span inherited the read-only fixed-EXIF chip's full pill styling (border / radius / padding / background) on top of the inner button's identical pill styling. Now the wrap is purely structural; the inner .chip-pill__btn carries the single pill.
  4. Drag-and-drop chip reorder via HTML5 native dnd (no library). Drop indicators (3 px progressive-blue bar) render on the relevant side of the hovered chip. Text segments stay pinned to their original positions; only chip identities at each slot swap, so the user's literal framing text survives the reorder.
  5. Default whitespace between chips: insertChipAt now adds a space when the caret touches a neighbouring chip with no intervening text (the chip-to-chip case the previous logic missed); .chip-pill-wrap also picks up a 2 px horizontal margin so adjacent chips never visually collide regardless of the text content between them.
  6. Blur-loses-changes (the show-stopper): the pre-fix editor used a setTimeout(120 ms) grace-period commit, but FixedPopout's outside-click handler tears the editor down synchronously, so the deferred commit ran on a null containerRef.current and silently bailed. Replaced with two synchronous code paths — (a) blur-commit using event.relatedTarget to bail when focus is moving to another descendant of the editor, otherwise commits immediately, mirroring what every other cell editor does; (b) useEffect-cleanup safety net so the chip popover's <select>-focused-then-clicked-outside case still persists (the text-input onBlur never fires there). Both paths share a committedRef latch so Enter / Escape / blur / unmount can race without double-commit. T426404 setState-loop pattern was checked and avoided.

CI: green on both commits (#187684, #187685).
Preview: https://upload-workbench.toolforge.org/mr-54/