feat(ag-ui): App-mode promo hero in sidebar mode#737
Conversation
Marketing surface for sidebar mode (App mode off): a preview-led poster with a static Paris map backdrop, Threadplane capability pills (client tools, generative UI, human-in-the-loop, shared state), and a CTA that enables App mode. Replaces the placeholder launcher hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5 tasks: TDD component (hasMapsKey input / enable output), wire into SidebarMode under the existing gate, e2e (key-independent), static map image with dark fallback + sips processing, verify + ship. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Final-review nits: add an (error) handler so a missing/404 map image hides cleanly to the dark fallback (no broken-image badge on the demo surface), and remove the color/font-size left on .sidebar-mode__background by the deleted launcher hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @blove's task in 2m 0s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
SummaryClean, well-scoped addition. The component is properly isolated (no shell coupling via DI), OnPush with signal inputs/outputs, a11y-aware (real Two minor observations (inline comments posted):
Everything else looks solid:
Result: LGTM with one minor test suggestion. The follow-up for the static map asset ( |
| src="/app-mode-preview.webp" | ||
| alt="Preview of the App-mode map cockpit" | ||
| loading="lazy" | ||
| (error)="promoImg.style.display = 'none'" |
There was a problem hiding this comment.
The direct DOM mutation in the error handler works and avoids a unnecessary component state property for this purely-visual side effect, so this is fine. One thing to be aware of: if the component is destroyed and re-created (e.g. after the @if gate toggles), the style.display = 'none' is gone and the broken image request fires again. Because the image URL is a static asset that either exists or doesn't, this is unlikely to be observed in practice, but if the asset lands later the behavior self-corrects on re-render. No action needed — just documenting the invariant.
| expect(el.querySelectorAll('.promo__pill').length).toBe(4); | ||
| const cta = el.querySelector('.promo__cta') as HTMLButtonElement | null; | ||
| expect(cta).toBeTruthy(); | ||
| expect(cta!.disabled).toBe(false); |
There was a problem hiding this comment.
Consider adding a companion assertion that the title attribute is absent (or empty) when the key is present — the current test only covers the false branch. A one-liner suffices:
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.hasAttribute('title')).toBe(false); |
Not blocking, but it closes the symmetry gap with the third test case.
Commit the real Paris map screenshot as the promo hero backdrop (public/app-mode-preview.jpg, 1600x594, ~360KB) and point the component <img> at it. Used JPEG (resized via sips) rather than webp — cwebp is broken on this machine (missing libtiff) and sips lacks webp output; the (error) dark-fallback still covers a missing asset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @blove's task in 1m 56s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
SummaryClean, well-scoped addition. The component is properly isolated (no shell coupling via DI), OnPush with signal inputs/outputs, a11y-aware (real Two issues posted as inline comments:
Everything else looks solid:
LGTM with one real fix ( |
| class="promo__img" | ||
| src="/app-mode-preview.jpg" | ||
| alt="Preview of the App-mode map cockpit" | ||
| loading="lazy" |
There was a problem hiding this comment.
loading="lazy" is counter-productive here. This image is the primary viewport content — it renders immediately when the component mounts, so the browser should fetch it eagerly. Lazy-loading defers the request until the browser's distance-from-viewport threshold is crossed, which can be satisfied after layout but still adds a tick or two of the dark fallback flash on slower connections. Remove the attribute (browsers default to eager) or switch to loading="eager". If you later add fetchpriority="high" it becomes the LCP hint too.
| loading="lazy" | |
| loading="eager" |
| expect(el.querySelectorAll('.promo__pill').length).toBe(4); | ||
| const cta = el.querySelector('.promo__cta') as HTMLButtonElement | null; | ||
| expect(cta).toBeTruthy(); | ||
| expect(cta!.disabled).toBe(false); |
There was a problem hiding this comment.
The enabled-key case only asserts disabled === false; it doesn't verify the title attribute is absent. Adding the companion assertion closes the symmetry gap with the third test case and ensures [attr.title] null-removal actually works.
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.hasAttribute('title')).toBe(false); |
- Embed/Popup toolbar buttons are no longer disabled while App mode is on. Choosing a full-chat layout now turns App mode off and shows the full chat. onModeChange navigates first, then flips appMode off so the persist effect's route sync resolves against the new route (no bounce to /sidebar). - Popup mode renders the App-mode promo hero behind the floating launcher (App mode off), mirroring sidebar mode; dead hint markup/styles removed. - e2e: promo visible in popup mode; choosing Embed from App mode lands on /embed with the app-body gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @blove's task in 2m 2s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
SummaryClean, well-scoped addition. The latest commit adds the promo to popup mode with symmetric wiring and covers it with two new e2e tests — the implementation pattern is identical to sidebar mode and looks correct. One real fix is still outstanding from the previous review; one minor test suggestion remains open. Issues1.
- loading="lazy"
+ loading="eager"2. Missing symmetrical The enabled-key test only checks expect(cta!.disabled).toBe(false);
+ expect(cta!.hasAttribute('title')).toBe(false);Popup-mode addition (new in this commit) ✓
Everything else ✓
Result: LGTM with one real fix ( |
App mode is no longer sidebar-only — it's now a property layered over either
chat presentation. Two knobs: the route (embed/popup/sidebar) is HOW the chat
is presented; App mode toggles WHAT fills the background (promo hero vs the
live map cockpit + itinerary overlay) behind popup/sidebar.
Decision matrix:
App mode OFF App mode ON
Embed full chat, no background not allowed (no background area)
Popup launcher + promo hero map cockpit + overlay; chat = launcher
Sidebar sidebar + promo hero map cockpit + overlay; chat = sidebar
Transitions:
- Toggle App mode in popup/sidebar: swap promo<->map, stay on the mode.
- Enable App mode from embed: auto-switch to sidebar (embed has no background).
- Popup <-> sidebar: App mode preserved. Embed always drops App mode.
- Reload restores (mode, appMode); embed forces App mode off.
Implementation:
- Persist effect no longer force-navigates /sidebar; it navigates to the
EXPLICIT current mode (read untracked) so reload restores popup/sidebar and
knob changes don't bounce the route. The crash-fix loader gate still holds.
- mode/appMode seed from window.location at bootstrap (router.url reads '/'
pre-initial-nav); appMode starts off if the route is embed.
- e2e: App mode in popup shows the map cockpit (promo replaced); popup<->sidebar
preserves App mode; embed drops it. Itinerary specs moved to
/sidebar?appmode=on (root /?appmode=on now resolves to embed -> App off).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @blove's task in 2m 55s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
SummaryThe latest commit ( Two issues from prior reviews remain unaddressed, and one new stale comment was introduced in the latest commit. Issues1. This image is the primary viewport content; it renders immediately on mount. Lazy-loading defers the fetch until the browser's distance-from-viewport threshold is crossed, adding a dark-fallback flash on slower connections. Remove the attribute or set 2. Missing symmetrical The enabled-key test checks 3. Stale comment in The comment says "App mode forces sidebar, so in popup mode it is effectively always off" — this was true before Everything else ✓
|
| class="promo__img" | ||
| src="/app-mode-preview.jpg" | ||
| alt="Preview of the App-mode map cockpit" | ||
| loading="lazy" |
There was a problem hiding this comment.
loading="lazy" is still counter-productive here (flagged twice in prior reviews). This image is the primary visible content — it mounts immediately when the component renders. Lazy-loading defers the fetch until the browser's distance-from-viewport threshold is crossed, adding a dark-fallback flash even though the element is already in view. Remove the attribute (browser default is eager) or switch to loading="eager".
| loading="lazy" | |
| loading="eager" |
| expect(el.querySelectorAll('.promo__pill').length).toBe(4); | ||
| const cta = el.querySelector('.promo__cta') as HTMLButtonElement | null; | ||
| expect(cta).toBeTruthy(); | ||
| expect(cta!.disabled).toBe(false); |
There was a problem hiding this comment.
Still missing the companion assertion that title is absent when the key is present (flagged three times across prior reviews). The test verifies disabled === false but doesn't confirm [attr.title] null-removal actually worked. One liner:
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.hasAttribute('title')).toBe(false); |
| <!-- The chat is a floating launcher here, so the area behind it markets | ||
| App mode (same hero as sidebar mode). App mode forces sidebar, so in | ||
| popup mode it is effectively always off — gate it the same way for | ||
| symmetry with sidebar mode. --> |
There was a problem hiding this comment.
This comment is now stale after commit 3465f40. "App mode forces sidebar, so in popup mode it is effectively always off" was true before but the latest commit explicitly makes App mode work in popup too (the map fills the background behind the floating launcher). The e2e test App mode runs in popup mode with the map cockpit behind the chat confirms this. The second and third sentences should be removed or rewritten.
| <!-- The chat is a floating launcher here, so the area behind it markets | |
| App mode (same hero as sidebar mode). App mode forces sidebar, so in | |
| popup mode it is effectively always off — gate it the same way for | |
| symmetry with sidebar mode. --> | |
The chat-select dropdown menu was an absolutely-positioned child, so it was clipped by any ancestor overflow container and trapped by any ancestor transform — e.g. the "More prompts" menu in sidebar mode was cut off by the sliding chat-sidebar panel (which has both overflow AND transform). position: fixed can't escape a transformed ancestor, so the menu must leave that context. Convert the menu to a CDK connected overlay (cdkConnectedOverlay) that portals to a body-level pane and uses a flexible position strategy (above-right preferred, flips/shifts to stay in the viewport). This fixes clipping for every chat-select consumer (welcome "More prompts", model picker in the input pill, both demo toolbars) and improves placement (toolbar selects now flip down instead of opening up into the top edge). - libs/chat: chat-select uses OverlayModule; menu styles drop self-positioning; options are queried via the menu view-ref (now portaled out of the host); outside-click close is handled by (overlayOutsideClick). New `panelClass` input lets consumers style the portaled pane. Theme tokens survive the portal because they're defined on :root, not :host. - @angular/cdk added as a peer dependency of @threadplane/chat. - Consumer menu-width overrides migrated from `::ng-deep chat-select .chat-select__menu` to the panelClass (both example welcome-suggestions). - specs/e2e updated to query the menu in the CDK overlay container. Verified: lint + unit (lib + both examples), ag-ui e2e 28/28, chat e2e 42/42, and live Chrome across all three consumer surfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @blove's task in 4m 39s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
SummaryThe latest commit ( Three issues from prior reviews remain unaddressed — inline comments posted on each. Outstanding issues (inline comments posted)1. Still counter-productive. Primary viewport content that mounts immediately; lazy-loading adds a dark-fallback flash for nothing. Fix this → 2. Stale comment in "App mode forces sidebar, so in popup mode it is effectively always off" contradicts the PR's own behaviour and the e2e test 3. Missing symmetrical Enabled-key case asserts LGTM with one real fix ( |
| class="promo__img" | ||
| src="/app-mode-preview.jpg" | ||
| alt="Preview of the App-mode map cockpit" | ||
| loading="lazy" |
There was a problem hiding this comment.
loading="lazy" is still counter-productive here — flagged in every prior review (×4 now). This image is the primary visible content and mounts immediately; lazy-loading only adds a dark-fallback flash on slower connections. Remove the attribute (the browser default is eager) or switch to loading="eager".
| loading="lazy" | |
| loading="eager" |
| App mode (same hero as sidebar mode). App mode forces sidebar, so in | ||
| popup mode it is effectively always off — gate it the same way for | ||
| symmetry with sidebar mode. --> |
There was a problem hiding this comment.
This comment is still stale — flagged in the previous review. "App mode forces sidebar, so in popup mode it is effectively always off" was true before commit 3465f40, but App mode now explicitly runs in popup (the e2e test App mode runs in popup mode with the map cockpit behind the chat confirms this). The @if gate itself is correct; the description of it is wrong.
| App mode (same hero as sidebar mode). App mode forces sidebar, so in | |
| popup mode it is effectively always off — gate it the same way for | |
| symmetry with sidebar mode. --> | |
| App mode (same hero as sidebar mode). The gate mirrors sidebar mode: while | |
| App mode is on the map fills the background, so the promo is not shown. --> |
| expect(el.querySelectorAll('.promo__pill').length).toBe(4); | ||
| const cta = el.querySelector('.promo__cta') as HTMLButtonElement | null; | ||
| expect(cta).toBeTruthy(); | ||
| expect(cta!.disabled).toBe(false); |
There was a problem hiding this comment.
Still missing the symmetrical title assertion (flagged ×3 in prior reviews). Closes the gap with test case 3 and verifies [attr.title]=null actually removes the attribute.
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.disabled).toBe(false); | |
| expect(cta!.hasAttribute('title')).toBe(false); |
Replace @threadplane/chat's @angular/cdk peer dep with a small public connected-overlay primitive (body portal + flip/clamp positioning + live reposition + a11y), using CDK overlay as the reference. chat-select is the first consumer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… portal, live reposition)
…l close-event handling Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…angular/cdk peer dep
- chat-select.styles.ts: fix stale "CDK" comment (now the in-lib overlay). - ag-ui e2e test-helpers selectToolbarOption: query .chat-overlay-container (the portaled menu), matching the chat twin. - Remove the obsolete manual toolbar menu-flip workaround from both shells — the connected-overlay positioner auto-flips downward, and the host-scoped ::ng-deep rule no longer reached the portaled menu anyway. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…promo # Conflicts: # examples/ag-ui/angular/src/app/modes/sidebar-mode.component.ts
…-merge with #741) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @blove's task in 1m 44s —— View job PR Review: feat(ag-ui): App-mode promo hero in sidebar mode
Applied fixesAll three outstanding review items are addressed in commit 1. The hero image is primary viewport content rendered immediately on mount; - loading="lazy"
+ loading="eager"2. Stale comment in "App mode forces sidebar, so in popup mode it is effectively always off" contradicted the PR's own behaviour (App mode now runs in popup too, confirmed by the e2e test). Updated to accurately describe the gate. -
+ 3. Symmetrical Closes the symmetry gap with test case 3; validates that expect(cta!.disabled).toBe(false);
+ expect(cta!.hasAttribute('title')).toBe(false);Push statusThe commit is ready locally but the GitHub App token returned a 403 on push (write permissions not granted for this run). Please run |
…742) Post-merge main CI fixes for the connected-overlay primitive (#737): - connected-overlay.directive.ts tripped @angular-eslint/no-input-rename / no-output-rename (errors in this repo) for the chatOverlay* binding aliases. Those aliases ARE the intended public API (namespacing under chatOverlay*, mirroring CDK's cdkConnectedOverlay* convention), so add a justified file-level eslint-disable for the two rules. - Regenerate chat api-docs.json for the new public exports (ChatConnectedOverlay/ChatOverlayOrigin directives + ConnectedPosition/ OverlayPositionResult types), which the "Verify generated API docs are committed" CI step requires. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What
Replaces the placeholder launcher hint in sidebar mode (App mode off) with a preview-led marketing hero that sells the App-mode map cockpit and the Threadplane primitives behind it, with a CTA that enables App mode.
AppModePromoComponent(modes/app-mode-promo.component.ts):hasMapsKeysignal input +enableoutput, OnPush, no shell coupling.object-fit: cover, dark fallback when absent) + an always-dark caption with a "Built with Threadplane" eyebrow, headline, four capability pills — Client tools, Generative UI, Human-in-the-loop, Shared state — and the primary CTA.SidebarModeunder the existing@if (shell.appMode() !== 'on')gate:[hasMapsKey]="shell.hasMapsKey",(enable)="shell.onAppModeChange('on')".GOOGLE_MAPS_API_KEYto enable" note (mirrors the toolbar toggle).<button>, focus-visible ring,aria-hiddenicons,prefers-reduced-motiongate,(error)guard hides a missing image to the dark fallback.Spec:
docs/superpowers/specs/2026-06-25-ag-ui-app-mode-promo-design.mdPlan:
docs/superpowers/plans/2026-06-25-ag-ui-app-mode-promo.mdVerification
enable; no-key disables CTA + shows note + title).app-mode-promo.spec.ts(promo visible in sidebar mode)./sidebar?appmode=oncockpit (map + overlay), promo gone.Follow-up (not blocking)
examples/ag-ui/angular/public/app-mode-preview.webp(drop the raw PNG; I'll resize/convert viasips). Until then the component shows its dark fallback — layout intact, no broken-image badge.🤖 Generated with Claude Code