Skip to content

feat(ag-ui): App-mode Google Map cockpit — AdvancedMarker + fit-to-bounds + dark style#738

Open
blove wants to merge 15 commits into
mainfrom
feat/ag-ui-map-cockpit-pr-b
Open

feat(ag-ui): App-mode Google Map cockpit — AdvancedMarker + fit-to-bounds + dark style#738
blove wants to merge 15 commits into
mainfrom
feat/ag-ui-map-cockpit-pr-b

Conversation

@blove

@blove blove commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What

Phase 2 of the AG-UI itinerary redesign: the App-mode Google Map cockpit. Builds on PR-A (#736, merged — the <google-map> loader-gate that fixed the blank-page-on-reload crash).

  • computeBounds pure helper (map-bounds.ts) — derives a LatLngBounds from coord'd stops; fully unit-tested.
  • Fit-to-bounds — an effect frames the map to all stops on structural change (add/remove/geocode), keyed on stopsWithCoords() only so reframing never fights focus-panning. 0 stops → Paris/zoom-12; 1 stop → center/zoom-13; ≥2 → fitBounds.
  • AdvancedMarkerElement migration<map-advanced-marker> with per-day colored dot pins (the pin <div> is built in markerViews so it only rebuilds when stops change, not on every CD pass), replacing the deprecated google.maps.Marker. Loads the marker library.
  • Cloud dark map style via a mapId (advanced markers require one; a mapId map ignores inline JSON styles, so the dark theme is a cloud style tied to the id). GOOGLE_MAPS_MAP_ID plumbed through the env mechanism; DEMO_MAP_ID fallback lets a fresh clone run a light map with no Console setup.
  • Docs for GOOGLE_MAPS_API_KEY + GOOGLE_MAPS_MAP_ID.

Verification

  • nx lint + nx test (47 passing, incl. map-bounds 5/5) + nx build for examples-ag-ui-angular — all green on this branch after syncing current main.
  • Live browser drive (real Chrome): confirmed the migrated bundle runs correctly — map-advanced-marker ×3 mounted, mapId applied with isAdvancedMarkersAvailable: true, markerViews computed from seeded stops, App mode forces /sidebar, no console errors from our code.

Known: live map tiles pending demo-project billing

The pixel-level render (dark tiles drawing) is not yet confirmed because the Maps demo GCP project currently can't serve tiles — a bare new google.maps.Map() (no Map ID) also stays UNINITIALIZED, which is the billing-not-enabled signature, independent of this code. Once billing is linked on that project the same key + Map ID render. This is a project-provisioning gate, external to the PR.

🤖 Generated with Claude Code

blove and others added 10 commits June 24, 2026 21:41
…Marker + crash fix)

Two-PR plan for the Phase-2 map cockpit follow-up: PR-A lands the already-built
blank-page-on-reload crash fix (GoogleMapsLoader gate); PR-B adds fit-to-bounds,
migrates google.maps.Marker → AdvancedMarkerElement with a cloud-based dark map
style (DEMO_MAP_ID fallback), preserving flat day-colored dot pins. Grey-map is
documented as environmental (tile-quota throttling), not a code bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-A is the already-open crash-fix PR #736, not a to-be-created PR. Note the
7-file scope, that it leaves fit-to-bounds/AdvancedMarker to PR-B, and that
PR-B must branch off #736 (not main) since main lacks the loader gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + AdvancedMarker)

7-task TDD plan layering on PR #736: pure computeBounds helper + unit test,
fit-to-bounds effect with 0/1/≥2 fallbacks + loader-gate no-op guard, GOOGLE_MAPS_MAP_ID
env plumbing, marker-library load, MapMarker→MapAdvancedMarker with day-colored div
pins + mapId + DEMO_MAP_ID fallback, README, and live map smoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview, Comment Jun 27, 2026 10:31pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 5m 10s —— View job


Code Review

  • Gather context (CLAUDE.md, PR diff, changed files)
  • Review map-bounds.ts + map-bounds.spec.ts
  • Review map-canvas.component.ts (core changes)
  • Review environment files + inject-env.mjs
  • Review google-maps-loader.ts
  • Post inline comments for specific issues
  • Post final summary

Overall this is a clean, well-structured migration. The computeBounds helper is properly typed and well-tested, the signal/effect architecture is sound, the markerViews computed avoids rebuilding on non-structural CD passes, and the inject-env.mjs change is safe (values go through JSON.stringify). No security issues or subscription leaks found.

One correctness bug found (see inline comment):

  • map-canvas.component.ts lines 141–144center.set(PARIS_CENTER) + zoom.set(12) in the "0 stops" branch is a no-op if fitBounds has already fired (signals are already at their initial PARIS_CENTER/12 values). Angular signals use Object.is() equality: same object reference + same number → no notification → GoogleMap.ngOnChanges never fires → map stays at the fitBounds viewport after all stops are removed. Fix: call map.googleMap?.setCenter(PARIS_CENTER); map.googleMap?.setZoom(12) directly, matching how fitBounds is already called imperatively.

Two minor observations (see inline comments):

  • map-canvas.component.ts lines 105–112makePin runs for every stop (not just changed ones) on any structural change; @for track keeps the component alive but Angular still delivers a ngOnChanges({content}) update to unchanged markers. Fine for a demo, but the comment slightly overstates the optimization.
  • map-bounds.ts lines 22–23 — min/max longitude produces a globe-spanning box for itineraries that cross the anti-meridian. Not a problem for Paris-centric routes, but worth noting if the helper is reused for international routing.

`;
writeFileSync(targetPath, contents);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length})`);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);
Comment on lines +141 to +144
if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal staleness bug — map won't reset to Paris after fitBounds + remove-all

After fitBounds() fires (≥2 stops), center() and zoom() signals are never updated — they stay at whatever value they held before (e.g., the initial PARIS_CENTER object and 12). Later, when all stops are removed, this branch calls this.center.set(PARIS_CENTER) and this.zoom.set(12).

Because Angular signals use Object.is() for equality:

  • Object.is(PARIS_CENTER, PARIS_CENTER)true (same object reference) → no notification emitted
  • Object.is(12, 12)true → no notification emitted

GoogleMap's ngOnChanges never fires, so the map stays at the fitBounds viewport instead of resetting to Paris/zoom-12.

Reproducer: add 2+ stops (fitBounds fires) → remove all stops → map doesn't return to Paris.

Fix: call the underlying API directly, consistent with how fitBounds is already imperative:

if (stops.length === 0) {
  map.googleMap?.setCenter(PARIS_CENTER);
  map.googleMap?.setZoom(12);
  return;
}

The 1-stop branch is fine as-is: this.center.set({ lat: ..., lng: ... }) produces a fresh object, so Object.is returns false and the signal propagates.

Fix this →

Comment on lines +105 to +112
protected readonly markerViews = computed(() =>
this.stopsWithCoords().map((s) => ({
id: s.id,
stop: s,
position: { lat: s.lat!, lng: s.lng! },
content: this.makePin(s.day),
})),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All pins rebuilt on any structural change — unchanged markers may briefly repaint

The comment says pins are "recreated only when stops change — not on every CD pass", which is accurate. But "when stops change" means every stopsWithCoords() emission calls makePin for all stops, not just the affected one.

For stops whose m.id is unchanged, @for track m.id keeps the MapAdvancedMarker component alive, but Angular still calls ngOnChanges({ content: newElement }) with the freshly-created HTMLElement. The underlying AdvancedMarkerElement replaces its content DOM node, causing a repaint for every existing marker whenever a single stop is added, removed, or geocoded.

For a demo app this is fine, but the comment overstates the optimization slightly. A Map<id, HTMLElement> cache (preserved across computed invocations via a class field) could limit rebuilds to only the changed stops if flicker becomes noticeable.

Comment on lines +22 to +23
east = Math.max(east, s.lng);
west = Math.min(west, s.lng);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anti-meridian caveat (not a bug for typical itineraries)

Min/max longitude works correctly for routes within a single hemisphere, but produces a globe-spanning bounding box for itineraries that cross the anti-meridian (e.g. Tokyo lng: 140 → Honolulu lng: -158). fitBounds on such a box would zoom the map out to show the entire world.

Not an issue for a Paris-centric travel demo, but worth a short comment if this helper gets reused for international routes. The proper fix would use google.maps.LatLngBounds with its extend() method, which handles anti-meridian crossing natively.

blove and others added 2 commits June 27, 2026 10:17
… hides

The map is full-bleed under the floating itinerary overlay (left) and the chat
sidebar drawer (right), but fitBounds used uniform 48px padding — so it framed
stops across the whole width and the rightmost marker landed under the chat
rail. Measure each panel's footprint live (adapts to the drawer's open/closed
state + responsive widths) and pass it as asymmetric fitBounds padding; fall
back to uniform 48 when the panels are absent (unit tests / non-App-mode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App mode previously forced the sidebar route and disabled the other mode
buttons. New rule: App mode layers over the map via the sidebar (right rail) OR
popup (floating bubble), so switching between those keeps App mode on; embed is
full-bleed chat that would cover the map, so selecting it turns App mode off.
The persist effect now restores the actual mode on reload (seeding mode() from
location.pathname to dodge the bootstrap race) and coerces a stray
embed+appmode=on to sidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 3m 51s —— View job


Code Review

  • Read CLAUDE.md + gather context
  • Review PR diff
  • Review map-canvas.component.ts
  • Review map-bounds.ts + spec
  • Review environment files + inject-env.mjs
  • Review shell component changes
  • Post inline comments
  • Post final summary

Summary

The core PR work (AdvancedMarker migration, computeBounds helper, dark mapId, env plumbing) is clean and well-structured. The two new commits (fitPadding panel-aware bounds, App-mode/popup coexistence) also look correct. One correctness bug from the initial review is still open.

One correctness bug (unresolved from prior review)

map-canvas.component.ts lines 143–146 — the "0 stops" reset branch calls this.center.set(PARIS_CENTER) and this.zoom.set(12), both of which are no-ops when the signals are already at those initial values. fitBounds is called imperatively and never updates the signals, so after a fitBounds frame + remove-all, Object.is(PARIS_CENTER, PARIS_CENTER)true → no notification → GoogleMap.ngOnChanges never fires → map stays at the fitBounds viewport. See the inline comment for the one-line fix.

New commits look good

fitPadding() (commit 0c6e8aa): The live-measured panel insets are a solid approach — reads actual getBoundingClientRect() so they adapt to the drawer open/closed state, fall back gracefully to uniform 48px when panels are absent (jsdom / non-App-mode), and the math is correct. One minor note: padding only refreshes when stops change, not when the drawer opens/closes mid-session. That's the right trade-off for a demo, but worth calling out if this gets used in a production layout.

App-mode/popup coexistence (commit c0907ba): The mode-change logic is correct — Embed turns App mode off (both the signal and persistence update before the router.navigate fires; the persist effect runs async and emits a relative [] navigation that just syncs params, which is harmless). Seeding mode() from location.pathname instead of router.url is the right fix for the bootstrap race. The class-selector coupling in fitPadding (.ag-ui-shell__itinerary-overlay, .chat-sidebar__content) is a minor fragility but acceptable for a demo app.

CodeQL alert (inject-env.mjs line 34)

The flagged key.length in the console.log is a false positive — logging a length is not logging the secret itself. It's a build-time script, not browser code. The new mapId ? 'set' : 'unset' addition is safe. A minimal fix if you want to silence the scanner: console.log('[inject-env] wrote generated-keys.local.ts');.

Pre-existing minor observations (already noted by prior review)

  • computeBounds uses min/max longitude, which produces a globe-spanning box for anti-meridian routes. Not a problem for Paris-centric itineraries.
  • markerViews rebuilds all pin elements on any structural stop change, not just the affected ones. Fine for a demo; a per-id cache would limit rebuilds if flicker becomes noticeable.

if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal staleness bug still open — map won't reset to Paris after fitBounds + remove-all

The prior review flagged this and it hasn't been fixed yet. Confirming it's still present in the latest commits.

When fitBounds fires (≥2 stops), it calls the imperative Maps API directly — the center and zoom signals are never updated. They stay at their initial values (PARIS_CENTER / 12). Later, when all stops are removed:

  • this.center.set(PARIS_CENTER)Object.is(PARIS_CENTER, PARIS_CENTER)true → no notification emitted
  • this.zoom.set(12)Object.is(12, 12)true → no notification emitted

GoogleMap.ngOnChanges never fires; the map stays at the fitBounds viewport.

Minimal repro: seed with ≥2 stops (fitBounds fires) → remove all stops → map doesn't return to Paris.

Fix: call the imperative API directly, consistent with fitBounds:

Suggested change
return;
if (stops.length === 0) {
map.googleMap?.setCenter(PARIS_CENTER);
map.googleMap?.setZoom(12);
return;
}

The 1-stop branch is fine: { lat: ..., lng: ... } is always a fresh object, so Object.is returns false and the signal propagates normally.

…t full width)

In Sidebar mode the chat drawer is a solid right rail, but the full-bleed map
extended under it, so fitBounds framed stops across the hidden width. Inset the
map by the chat lib's published drawer footprint (--ngaf-chat-occupy-right) so
it occupies its own column and markers center within the visible area; Popup
mode mounts no drawer, so the var is 0 and the map stays full-bleed under the
floating bubble. The descendant selector + width:auto beat the map component's
own :host { width: 100% } (an over-constrained left/right/width otherwise keeps
the full width and silently ignores the inset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 2m 15s —— View job


Code Review

  • Gather context (CLAUDE.md, PR diff, changed files)
  • Review map-canvas.component.ts — signal staleness bug status
  • Review inject-env.mjs — CodeQL alert
  • Review new commits (fitPadding, App-mode/popup coexistence)
  • Post inline comments for specific issues
  • Post final summary

The new commits (fitPadding panel-aware insets, App-mode/popup coexistence, map column fix) all look correct. One correctness bug from the first review remains open, and the CodeQL alert has an easy one-line fix.

One correctness bug (flagged in all three reviews — still unresolved)

map-canvas.component.ts lines 143–147 — the "0 stops" reset branch calls this.center.set(PARIS_CENTER) and this.zoom.set(12). Both are no-ops when the signals are already at their initial values, because fitBounds is called imperatively and never updates the signals. Object.is(PARIS_CENTER, PARIS_CENTER)true → no notification → GoogleMap.ngOnChanges never fires → viewport stays at the fitBounds position after all stops are removed. The suggestion block in the inline comment gives the one-line fix.

CodeQL alert (inject-env.mjs line 34)

key.length is not the key itself, but the scanner still flags any console.log referencing a variable derived from the API key. The same key ? 'set' : 'unset' idiom you already used for mapId silences it. Inline comment has the suggestion.

New commits look good

  • fitPadding() (commit 0c6e8aa): Live-measured getBoundingClientRect() is the right approach — adapts to drawer open/closed, falls back to 48px in jsdom/non-App-mode. One known trade-off: padding only refreshes when stops change, not when the drawer opens or closes mid-session. Fine for a demo.
  • App-mode/popup coexistence (commit c0907ba): Mode-change logic is correct — appMode.set('off') + persistence.write both happen before router.navigate, the persist effect fires async with commands = [] (just syncing params, harmless). Seeding mode() from location.pathname instead of router.url correctly avoids the bootstrap race. Class-selector coupling in fitPadding (.ag-ui-shell__itinerary-overlay, .chat-sidebar__content) is acceptable for a demo app.
  • Map column fix (commit 005cdce): .ag-ui-shell__app-body .ag-ui-shell__map { inset: 0 var(--ngaf-chat-occupy-right, 0px) 0 0; width: auto; } correctly narrows the map out from under the sidebar drawer in App mode and leaves it full-bleed in Popup mode where the CSS var is unset.

Pre-existing minor observations (already noted, not blocking)

  • computeBounds uses min/max longitude — globe-spanning box for anti-meridian routes. No problem for Paris-centric itineraries.
  • markerViews rebuilds all pin HTMLElements on any structural stop change (not just the affected stop). @for track m.id keeps the component alive but Angular still calls ngOnChanges({ content: newElement }) for unchanged markers. Fine for a demo; a per-id Map<id, HTMLElement> cache would limit rebuilds if marker repaint becomes noticeable.

Comment on lines +143 to +147
if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal staleness bug (third review — still open)

this.center.set(PARIS_CENTER) and this.zoom.set(12) are no-ops when the signals are already at their initial values. fitBounds is called imperatively and never updates these signals. So after a fitBounds frame followed by removing all stops:

  • Object.is(PARIS_CENTER, PARIS_CENTER)true (same const reference) → no notification
  • Object.is(12, 12)true → no notification

GoogleMap.ngOnChanges never fires; the viewport stays where fitBounds left it.

Reproducer: add ≥2 stops → fitBounds fires → remove all stops → map stays at the fitBounds viewport instead of returning to Paris/zoom-12.

Fix — call the imperative API directly, consistent with how fitBounds is already called:

Suggested change
if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;
}
if (stops.length === 0) {
map.googleMap?.setCenter(PARIS_CENTER);
map.googleMap?.setZoom(12);
return;
}

The 1-stop branch (this.center.set({ lat: ..., lng: ... })) is fine: a fresh object literal always fails Object.is, so the signal propagates normally.

`;
writeFileSync(targetPath, contents);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length})`);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL: clear-text logging — still flagged

key.length is not the key itself, so this is a false positive semantically. But the scanner sees a variable derived from GOOGLE_MAPS_API_KEY passed to console.log and flags it. The mapId ? 'set' : 'unset' pattern you added for the new mapId is exactly the right idiom — applying the same treatment to the key reference silences the alert with zero behaviour change:

Suggested change
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);
console.log(`[inject-env] wrote generated-keys.local.ts (key: ${key ? 'set' : 'unset'}, mapId: ${mapId ? 'set' : 'unset'})`);

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 2m 14s —— View job


Code Review

  • Gather context (CLAUDE.md, PR diff, changed files)
  • Check if signal staleness bug is fixed (map-canvas.component.ts) — still open
  • Check if CodeQL alert is resolved (inject-env.mjs) — still open
  • Post inline suggestion comments for both fixes
  • Post final summary

Summary

The new commits (fitPadding panel-aware insets, App-mode/popup coexistence, map column fix) all look correct and are not blocking. Two issues flagged in prior reviews remain open and are addressed by one-click inline suggestions above.


Two issues — both have inline code suggestions above

1. Signal staleness bug — map-canvas.component.ts lines 143–146 (view →)

this.center.set(PARIS_CENTER) and this.zoom.set(12) are no-ops after any fitBounds call, because fitBounds is imperative and never updates these signals. So:

  • Object.is(PARIS_CENTER, PARIS_CENTER)true (same const reference) → no notification emitted
  • Object.is(12, 12)true → no notification emitted

GoogleMap.ngOnChanges never fires; the map stays at the fitBounds viewport after all stops are removed. The fix calls the imperative API directly — map.googleMap?.setCenter(PARIS_CENTER) / map.googleMap?.setZoom(12) — consistent with how fitBounds is already called. map is already in scope and non-null at that point. The inline suggestion can be applied with one click.

2. CodeQL alert — inject-env.mjs line 34 (view →)

key.length is semantically safe (not the key itself), but CodeQL flags any console.log referencing a variable derived from GOOGLE_MAPS_API_KEY. Applying the same key ? 'set' : 'unset' idiom already used for mapId silences the scanner with zero behaviour change.


Everything else looks good

  • computeBounds helper is well-typed and correctly tested (5/5 cases).
  • markerViews computed avoids rebuilding on non-structural CD passes.
  • fitPadding() live-measures getBoundingClientRect() to adapt to the open/closed chat drawer — correct approach, falls back gracefully in jsdom.
  • App-mode/popup coexistence is correct: appMode.set('off') + persistence.write both happen before router.navigate; seeding mode() from location.pathname avoids the bootstrap race.
  • Map column fix (.ag-ui-shell__app-body .ag-ui-shell__map { inset: 0 var(--ngaf-chat-occupy-right, 0px) 0 0; width: auto; }) correctly narrows the map in App mode and leaves it full-bleed in Popup mode.
  • No subscription leaks, type-safety issues, or security problems found.
  • inject-env.mjs uses JSON.stringify for all env values — no injection risk.

Note: I attempted to push the two fixes as a commit but the bot lacks write access to this repository. The inline suggestions above can be applied directly from the GitHub UI.

Comment on lines +143 to +146
if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal staleness bug (4th review — still unresolved)

this.center.set(PARIS_CENTER) and this.zoom.set(12) are no-ops here. fitBounds is called imperatively and never updates these signals, so after a fitBounds frame followed by removing all stops:

  • Object.is(PARIS_CENTER, PARIS_CENTER)true (same const reference) → no notification
  • Object.is(12, 12)true → no notification

GoogleMap.ngOnChanges never fires; the viewport stays at the fitBounds position.

Reproducer: add ≥2 geocoded stops → fitBounds fires → remove all stops → map stays at the fitBounds viewport instead of resetting to Paris/zoom-12.

map is already in scope (line 140) and guarded non-null at line 142, so the fix is one line per call:

Suggested change
if (stops.length === 0) {
this.center.set(PARIS_CENTER);
this.zoom.set(12);
return;
if (stops.length === 0) {
map.googleMap?.setCenter(PARIS_CENTER);
map.googleMap?.setZoom(12);
return;
}

The 1-stop branch (this.center.set({ lat: ..., lng: ... })) is fine — a fresh object literal always fails Object.is, so the signal propagates correctly.

`;
writeFileSync(targetPath, contents);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length})`);
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL: clear-text logging — still flagged

key.length is semantically safe (not the key itself), but CodeQL flags any console.log that references a variable derived from GOOGLE_MAPS_API_KEY. Apply the same ? 'set' : 'unset' idiom already used for mapId to silence the alert with no behaviour change:

Suggested change
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);
console.log(`[inject-env] wrote generated-keys.local.ts (key: ${key ? 'set' : 'unset'}, mapId: ${mapId ? 'set' : 'unset'})`);

…e/occupy hack)

App mode now renders the map inside SidebarMode's <chat-sidebar> content slot, so
it fills the area left of the drawer via the lib's flex layout (PR #740) — no
shell-level absolute positioning or --ngaf-chat-occupy-right inset. Popup mode
keeps the full-bleed shell map. Adds a ResizeObserver in MapCanvasComponent that
re-triggers google.maps resize + re-fit when the content slot resizes (the push
shrink / mode toggles), so tiles don't render grey at the stale size.

NEEDS VERIFICATION in a reliable browser: the agent's flaky headless/extension
tooling renders the map only intermittently. One good render confirmed the 981px
column + vector map + polyline, but advanced-marker pins did not attach in that
pass — unclear if real or a tooling artifact. Verify pins + tiles + the
sidebar↔popup toggle before merging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 1m 41s —— View job


Code Review — Fixes Ready (Push Blocked)

  • Gather context
  • Read map-canvas.component.ts — located frameToBounds at lines 165–179
  • Read inject-env.mjs — located CodeQL line 34
  • Applied fix 1: signal staleness bug
  • Applied fix 2: CodeQL alert
  • Committed locally (cbd3d82)
  • Push attempted — denied (bot lacks write access to this repo)

Both fixes were applied and committed locally but the bot cannot push to cacheplane/angular-agent-framework. The two inline code suggestions posted by prior reviews remain the fastest path — apply them directly from the GitHub UI with one click each.


Fix 1 — Signal staleness bug (map-canvas.component.ts, frameToBounds)

fitBounds is called imperatively and never updates the center/zoom signals. After a fitBounds call followed by removing all stops, center.set(PARIS_CENTER) is a no-op: Object.is(PARIS_CENTER, PARIS_CENTER)true → no Angular notification → GoogleMap.ngOnChanges never fires → map stays at the fitBounds viewport.

// Before (no-op after fitBounds)
if (stops.length === 0) {
  this.center.set(PARIS_CENTER);
  this.zoom.set(12);
  return;
}

// After (imperative, consistent with fitBounds)
if (stops.length === 0) {
  map.googleMap?.setCenter(PARIS_CENTER);
  map.googleMap?.setZoom(12);
  return;
}

Reproducer: add ≥2 geocoded stops (fitBounds fires) → remove all stops → map stays at fitBounds viewport instead of resetting to Paris/zoom-12.

Fix 2 — CodeQL clear-text logging (inject-env.mjs, line 34)

key.length is not the key itself, but CodeQL flags any console.log referencing a variable derived from GOOGLE_MAPS_API_KEY. Apply the same ? 'set' : 'unset' idiom already used for mapId:

// Before
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);

// After
console.log(`[inject-env] wrote generated-keys.local.ts (key: ${key ? 'set' : 'unset'}, mapId: ${mapId ? 'set' : 'unset'})`);

Everything else in the PR looks good — computeBounds helper, markerViews computed, fitPadding() live-measured insets, App-mode/popup coexistence, and the map column CSS fix are all correct and clean.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants