feat: add generalized map widget (THU-605)#1000
Conversation
Bring the map widget prototyped on the file-upload demo branch into the product as a first-class, demo-agnostic widget. - Add src/widgets/map: a generic GeoJSON FeatureCollection renderer on MapLibre GL (lazy-loaded), with schema, parser, instructions, and tests. - Register `map` in the widget registry (src/widgets/index.ts). - Add maplibre-gl dep. The widget carries no demo-specific coupling. Note: the basemap uses the public OpenFreeMap Positron tiles; swapping to a production-grade tile source is tracked separately on THU-605. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Semgrep Security ScanNo security issues found. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f0041ed. Configure here.
| 'circle-stroke-color': '#ffffff', | ||
| 'circle-stroke-width': 2, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Multi geometries never render
High Severity
Layer filters only match Point, LineString, and Polygon via geometry-type, but validated GeoJSON can include MultiPoint, MultiLineString, and MultiPolygon. MapLibre reports those Multi types separately, so those features never draw despite schema and AI instructions claiming Multi* support.
Reviewed by Cursor Bugbot for commit f0041ed. Configure here.
| } | ||
| // Reset to the skeleton whenever the data changes and we re-init the map. | ||
| setReady(false) | ||
| let map: MaplibreMap | null = null |
There was a problem hiding this comment.
Map error state never clears
Medium Severity
After setError runs on a failed MapLibre load, a later successful init never resets error. The effect only calls setReady(false) when data changes, so users can see “Couldn’t load the map” under a map that loaded correctly.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f0041ed. Configure here.
| const popup = new Popup({ closeButton: false, maxWidth: '300px' }) | ||
| .setLngLat(event.lngLat) | ||
| .setDOMContent(node) | ||
| .addTo(map) |
There was a problem hiding this comment.
Each click spawns new popup
Low Severity
showPopup creates a new Popup on every feature click without removing the previous one. Clicking several markers can stack multiple popups until the user clicks the bare map.
Reviewed by Cursor Bugbot for commit f0041ed. Configure here.
| if (!cancelled) { | ||
| setReady(true) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Load handler ignores cancellation
Medium Severity
The map load callback runs addSource and addLayer without checking cancelled, while cleanup sets cancelled and calls map.remove(). If load fires after teardown, it may touch a removed map or call setReady on a stale instance.
Reviewed by Cursor Bugbot for commit f0041ed. Configure here.
|
Preview environment deployed 🚀
Stack: Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — |
| // blank/white map if it initialized before layout settled or while briefly | ||
| // hidden (e.g. switching away and back to a chat) and is never told to | ||
| // resize. The observer fires on the size change and re-renders at size. | ||
| const resizeObserver = new ResizeObserver(() => map?.resize()) |
PR Metrics
Updated Thu, 18 Jun 2026 17:54:43 GMT · run #1947 |


What
Brings the map widget prototyped on the file-upload demo branch into the product as a first-class, demo-agnostic widget.
src/widgets/map: a generic GeoJSONFeatureCollectionrenderer on MapLibre GL (lazy-loaded), with schema, parser, instructions, stories, and tests.mapin the widget registry (src/widgets/index.ts).maplibre-gldep.No demo-specific coupling — the widget renders arbitrary GeoJSON the model emits via
<widget:map .../>.Follow-up
The basemap currently uses the public OpenFreeMap Positron tiles. Swapping to a production-grade tile source is tracked on THU-605 and intentionally out of scope for this extraction.
Test
src/widgets/map/geojson.test.ts: 10/10 pass; typecheck clean.Closes THU-605
🤖 Generated with Claude Code
Note
Low Risk
Self-contained UI widget with validated GeoJSON and safe popup rendering via textContent; no auth or backend changes. Main caveat is reliance on third-party tile URLs and a larger client bundle when maps are shown.
Overview
Adds a
mapchat widget so agents can render locations via<widget:map data='…' />with a GeoJSON FeatureCollection (and optional title).The new
src/widgets/mapmodule follows the same pattern as other widgets: Zod schema/parser, AI instructions, MapWidget UI, Storybook stories, and geojson helpers (parse, bounds, labels). The map uses lazy-loaded MapLibre GL, fits the view to features, supports Point/Line/Polygon (and Multi*), simplestyle-spec styling, and click popups for genericlabel/descriptiononly.mapis registered inwidgetRegistryandmaplibre-glis added as a dependency.Basemap tiles use the public OpenFreeMap Positron endpoint (noted as a follow-up for production tile sources).
Reviewed by Cursor Bugbot for commit f0041ed. Bugbot is set up for automated code reviews on this repo. Configure here.