Stripe's compliance crawler fetches the apex URL without executing JS and
declined live-mode review when `https://resolutionflow.com/` returned the
empty SPA shell that redirected to /landing client-side. Restructure the
router so / serves LandingPage directly:
- `/` → new `PublicLanding` wrapper (LandingPage for anon; Navigate to
/home for authed users so there's no marketing-frame flicker).
- Authed tree converted to a path-less layout route with absolute child
paths. QuickStartPage moves to `/home`; all other children
(`/trees`, `/pilot`, `/admin/*`, `/account/*`, etc.) keep their URLs.
- `/landing` kept as a one-release stale-bookmark redirect to /.
- `ProtectedRoute` unauth redirect flipped /landing → /; `state.from`
preserved for post-login return.
Reference updates:
- Post-login / post-onboarding destinations → /home: OAuthCallbackPage
(incl. `?welcome=teammate` query), WelcomeStep1/2/3 dismiss-rest,
AssistantChatPage post-escalate, WelcomeRouter completion/dismiss
redirects, VerifyEmailPage's three "Go to dashboard" links.
- Authed chrome → /home: TopBar logo, AppLayout mobile nav + drawer
logo, CommandPalette Dashboard entry.
- Dashboard onboarding → /home: NextStepCard `ran_session.ctaPath`,
SetupChecklist `ran_session.path`, SessionHistoryPage empty-state CTA.
- Public back-links → /: TermsPage, PrivacyPage, PoliciesPage,
ContactPage, PromotionsPage, PublicTemplatesPage (header + footer).
SharedSessionPage's `to="/"` left as-is — now correctly lands anon
visitors on the public landing.
Crawlability:
- New `frontend/public/robots.txt` allowlisting public pages and
disallowing the authed app.
- New `frontend/public/sitemap.xml` for /, /pricing, /contact-sales,
/contact, /templates, /terms, /privacy, /policies, /promotions.
- `PageMeta` gains an `og:url` (defaults to `window.location.href`) and
flips `twitter:card` to `summary_large_image` when an `ogImage` is
passed.
Tests:
- `AppLayout.test.tsx` updated to mount at `/home`.
- New `ProtectedRoute.test.tsx` asserts unauthenticated `/home`
redirects to `/` (not `/landing`) and preserves origin in `state.from`.
If Stripe's crawler still cannot see the site after this (zero-JS
crawler), the documented next escalation is server-side prerendering of
public routes via `vite-plugin-ssg`. Out of scope here.
Plan: docs/plans/2026-05-13-public-landing-routing-refactor.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "Start a session" CTAs on the NextStepCard and SetupChecklist used to
Link-navigate, which left the user on the same page (the Start Session
input lives on the dashboard) without any visible response. Replace those
CTAs with a custom window-event dispatch (FOCUS_START_SESSION_EVENT) that
the StartSessionInput listens for: scroll the input into view, focus the
textarea, and pulse a ring for 900ms so the click feels intentional. The
NextStepCard also locally hides itself after firing so the user isn't
double-prompted while typing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Eighth commit in the session-expiration-policy series. Surfaces all
the owner controls and user-facing expiry UX that the prior commits
plumbed through, designed end-to-end via /plan-design-review (initial
4/10 -> final 9/10; 7 decisions locked in the plan).
Backend additions:
- accounts/me/security GET response gains active_users: list of
{user_id, name, email, last_login_at} for users in this account
with at least one un-revoked refresh token. Joined query on
refresh_tokens + users, distinct, ordered by last_login desc.
Drives the Active Sessions section.
Frontend additions:
- api/accountSecurity.ts: typed client for GET/PATCH/revoke-sessions.
- hooks/useAuthSessionExpiry.ts: reads idle/absolute expiry from the
auth store, returns warning ('none'|'soon'|'now') + reason
('idle'|'absolute') so consumers can pick the right UX for the
closer window. Re-evaluates every 30s.
- components/common/SessionExpiryToast.tsx: top-of-app notice that
fires at T-5min. Idle case: warning-amber tone, [Stay signed in]
button hits authApi.refresh() and updates the store on success.
Absolute case: info-cyan tone, [Sign in now] link to /login (no
recoverable action). Dismissable, doesn't re-fire after dismissal.
- components/account/RevokeSessionsModal.tsx: confirmation modal for
the two bulk-revoke scopes. Title, body, and confirm-label vary by
scope; danger-styled confirm button.
- pages/account/AccountSecuritySettingsPage.tsx: the main page.
Header (Shield icon), intro, Policy card with Strict/Standard/Custom
radios + always-visible-disabled Custom inputs (idle/absolute
minutes) with inline validation, Save button + emerald success ping,
info note about 'applies at next login'. Active sessions card with
count-aware copy, list of {name, email, last-login-ago} rows
(caller tagged '(you)'), two buttons — 'except me' hidden when
count=1, 'sign me out and everyone else' uses danger-tinted styling.
- pages/AccountSettingsPage.tsx: 'Session security' row added to the
owner-only settings list.
- router.tsx: /account/security route, owner-gated via ProtectedRoute.
- pages/LoginPage.tsx: cyan info-tone banner above form when
?reason=session_expired is in the URL.
- components/layout/AppLayout.tsx: mounts <SessionExpiryToast />.
Scope=all bulk-revoke UX (the most jarring moment): on success,
toast.success(N sessions), 1.5s delay, then clear localStorage +
useAuthStore.logout() + window.location='/login' (no banner — the
owner just did this).
Backend tests: existing 22/22 still green plus the GET test now
asserts active_users is present + non-empty after login. Frontend:
tsc clean, authStore test 2/2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the three legal/contact pages needed for Stripe live-mode site review: /policies (consolidated customer policies — refunds, cancellation, legal restrictions, promotions), /contact (phone (470) 949-4131 + support/sales/billing/security inboxes), /promotions (stub satisfying §6.2 cross-ref).
Extracts the existing landing footer into components/common/MarketingFooter.tsx and mounts it on /pricing and /contact-sales so all four legal links are reachable from every marketing surface.
Privacy and Terms closing sections updated to point at /contact + /policies; stale hello@ mailto removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Michael Chihlas <michael@resolutionflow.com>
Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
- Sidebar: kill the drifting railGroups + sections dual definition.
Single source of truth (workItems / libraryItems / footerItems)
rendered in both pinned and rail modes; pin/unpin is a width and
label affordance, not an IA switch. Hairline divider replaces
section labels. Guides moves to the footer alongside Account.
Renames: Home -> Dashboard, History -> Sessions, Insights -> Analytics.
- CURRENT-STATE.md: log PR #158 (session impeccable pass + tasklane
keyboard flow) under "Recently shipped".
- PRODUCT.md: design-context source of truth (users, brand, aesthetic);
sibling to DESIGN-SYSTEM.md.
- skills-lock.json: lock /impeccable + /documentation-writer skill
versions so other sessions reproduce the same tooling state.
- Drop stale .impeccable.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace 15 feature-dump guides with 43 problem-oriented how-tos grouped
under 10 categories. Drop Maintenance Flows / AI Assistant / Flow Assist
Sparkles — those surfaces no longer exist post-FlowPilot pivot. Rename
Step Library → Solutions Library throughout. Correct every "click X in
the sidebar" reference to match live labels (Home, History, Tickets,
Flows, Scripts, Data, Acct).
Schema: add `category: CategoryId` and optional `relatedSlugs` to Guide;
new Category type and `categories` const drive hub ordering. GuidesHubPage
renders category sections (auto-hides empty); GuideDetailPage renders a
related-guides footer when set; GuideCard drops the misleading "N sections"
subtitle.
Fix step.tip markdown rendering — `**bold**` rendered literally because
tip used plain text instead of the same regex replacement used on
instruction.
14 net-new how-tos for FlowPilot-era surfaces with no prior coverage:
tasklane keyboard flow, view-what-we-know, ask-AI mid-session,
pause-and-leave, resolve, record-fix-outcome, escalate (Escalation
Mode), post-docs-to-ticket, send-client-update, build-script-from-scratch,
open-suggested-flow, pin-a-flow, invite-teammate.
Browser-verified against engineer + owner test users (sidebar labels,
account sub-pages, pilot-screen header buttons, Tasks panel, integration
form). tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small ergonomic fixes after the impeccable pass:
- TaskLane keyboard hints (⏎ submit · ⇧⏎ newline) under each open input
were rendered at text-muted-foreground/70, just shy of legible at a
glance. Drop the /70 opacity modifier so they read at full muted weight
on first look without becoming visually loud.
- 12 sites across the session screen had explicit font-sans utilities,
but the body default is already IBM Plex Sans (via --font-sans in
index.css and Tailwind v4's default-sans binding). None of the call
sites sit inside a font-heading or font-mono cascade, so every
font-sans there was a no-op. Drop them. ConcludeSessionModal also had
three "text-xs font-sans text-xs" triplets — drop both the redundant
font-sans and the doubled text-xs in one pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ParameterizationPreview.tokenize() matched highlight values via raw
seg.text.startsWith(value, cursor) with no word-boundary check and no
minimum length. A param value like "D" (e.g. a drive letter) lit up every
capital D in the script body — Get-ADUser, Add-Type, Disable- all rendered
as proposed-parameter pills.
Add a word-boundary guard: a candidate match is only accepted if either
side of the match either falls at start/end of the segment, OR the
adjacent character is non-alphanumeric. The guard is conditional on
whether the value itself starts/ends with a word char, so values that
begin or end in punctuation (e.g. "D:\\Folder") still match cleanly when
they sit next to whitespace or punctuation.
Surfaced 2026-05-01 while testing the suggested-fix flow with a real
PowerShell script.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Multi-step UX refactor of the assistant chat session screen, run via the
$impeccable skill. Heuristic score moved 24/40 → 33/40 (+9), with the biggest
gains on Aesthetic & Minimalist (1→3), Consistency & Standards (1→3), and
Recognition Rather Than Recall (2→4).
Distill — chat region:
- Remove the "Suggested checks" chip strip + selected-chip detail card; the
TaskLane is the single canonical home for "what to do next"
- Add an inline Next steps · N pending cue above the latest action-bearing
AI bubble (anchors attention without duplicating the lane's items)
- Link banner ↔ script-panel lifecycle: collapsing or dismissing the
ProposalBanner now also hides the InlineNoTemplateDialog / TemplateMatchPanel
- Drop backdrop-blur on the handoff-context overlay (DESIGN-SYSTEM hard rule)
Quieter — drop decoration overshoot:
- Remove 3px side stripes on TaskLane done cards, all 6 ProposalBanner modes,
WhatWeKnowItem fact rows
- Drop bg-gradient surfaces on WhatWeKnow + every ProposalBanner mode
- Drop 2px accent borderTop on the TaskLane header
- Replace bordered avatar boxes in banners with inline state-colored icons
- Each surface now uses a single decoration channel (top border + inline icon)
Layout:
- Header consolidates to Resolve + Escalate + ⋯ kebab; Context, New Ticket,
Update Ticket, Pause now live behind the kebab on desktop, with feature
parity in the existing mobile overflow menu
- Messages column anchors to max-w-3xl mx-auto to match the composer
- Chat bubbles drop from rounded-2xl to rounded-xl for vocabulary alignment
Typeset:
- Unify text sizing from 14 distinct sizes (with sub-pixel oddities and
rem/px duplicates) to a 5-step scale: 10px / 11px / text-xs / 13px / text-sm
WhatWeKnow collapsible:
- Header is now a toggle; section body hides when collapsed
- Auto-collapses on first render when facts ≥ 5 so Questions / Diagnostic
Checks stay above the fold
- Engineer's choice persists in sessionStorage per session and beats the
auto-collapse heuristic on subsequent renders
- key=activeChatId on both render sites resets state cleanly across sessions
Polish:
- Split MessageCircleQuestion into Pencil (question Answer CTA, write
affordance) + HelpCircle (per-check Explain toggle, universal help icon) —
same icon for two different jobs was a discoverability bug
- Drop redundant text-xs from font-sans text-[0.625rem] / text-[0.6875rem]
double-class definitions; the more-specific size always wins
TaskLane keyboard flow:
- Enter submits and auto-advances to the next pending task; Shift+Enter
inserts a newline (consistent across question and action textareas — paste
events don't fire keydown, so paste-then-Enter still works as expected)
- Esc cancels (same as the Cancel button)
- After the last pending task is submitted, focus moves to the Send Responses
button so the engineer can fire the whole batch with one more keystroke
- Subtle hint row under each open input teaches the shortcut
Type-check, lint, and build all clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Page-level Resolve patches applied_pending → applied_success before
opening the resolution flow, so resolved sessions don't carry a
provisional pending fix.
- Page-level Escalate intercept now catches applied_pending in addition
to verifying/partial; intercept copy generalized from "Verifying state"
to "still needs an outcome."
- PendingBanner gains a Dismiss action, matching the PR body and the
backend's allowed pending → dismissed transition.
- resolution_note_generator and escalation_package_generator system
prompts no longer include real-looking pending examples (anti-parrot
guardrail compliance).
Verified via Docker: prompt anti-parrot 2/2, suggested-fix outcome suite
21/21, frontend tsc -b clean, npm run build clean.
Co-Authored-By: Codex <noreply@openai.com>
Engineer applies a fix but can't verify yet (waiting on client power-cycle,
AD replication, async sync). Today the verifying banner forces a synchronous
verdict (worked / didn't / partial) — anything else means leaving the banner
stale or guessing wrong. This adds a fourth outcome that parks the fix in a
non-terminal "Awaiting verification" state with a reason ("waiting on what?")
and exposes it on the chat-anchored banner so the engineer doesn't lose track.
Backend
- New non-terminal status `applied_pending` parallel to `applied_partial`.
- New `pending_reason` column (nullable Text) — the "what are you waiting on?"
prose, mirrors `partial_notes`. Required when outcome=applied_pending.
- Outcome endpoint allows pending in/out transitions; pending stamps
applied_at but NOT verified_at (it's parked, not verified).
- Resolution-note + escalation-package prompts handle the new status:
resolution note frames the fix as provisional; escalation package surfaces
pending verification as the leading hypothesis with reference to what's
being waited on.
- Migration: add column + extend status CHECK constraint.
Frontend
- New `BannerMode = 'pending'` + `PendingBanner` component (info-tone,
parallel to PartialBanner) with worked / didn't / update-reason actions.
- VerifyingBanner overflow menu adds "Waiting to verify…".
- Nudge banner's "Still checking" button now actually records pending with
a reason, instead of just silencing for the session.
- AssistantChatPage banner-mode derivation maps applied_pending → 'pending'.
Tests: 4 new integration tests covering pending notes requirement, reason
storage + applied_at/verified_at semantics, pending→success transition,
and pending_reason update on re-PATCH.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codex review pass on the escalation wedge. Reworks claim_session from
read-then-write to a conditional UPDATE so two seniors racing can't both
win, blocks the original engineer from claiming their own handoff, and
filters self-escalated sessions out of the dashboard escalation queue.
Also preassigns the handoff UUID before flush so the compatibility
escalation_package payload carries it. Removes legacy frontend pickup
state (claiming, handleStartHere) that broke tsc --noEmit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundles four fixes from the live debugging session:
1. AssistantChatPage: replace urlSessionId === activeChatId gate with a
loadedChatIdsRef. After 8914391 made activeChatId initialize from
urlSessionId, the gate short-circuited fresh mounts and selectChat
never fired. Symptom: senior picks up an escalation, lands on a blank
chat surface with no conversation history and no sidebar entry. Fix
also adds loadChats() in handleStartHere so the picked-up session
appears in the sidebar (its escalated_to_id is null pre-claim, so
listSessions doesn't return it until claim_session sets it).
2. config: bump ESCALATION_AI_ASSESSMENT_TIMEOUT_SECONDS 15s → 45s.
Sonnet was hitting tail latency at 15s in the field, leaving the
magic-moment placeholder permanent. Background-task architecture
(e8ba74e) means this no longer blocks the user; it's just the budget
before publishing has_assessment=false. NOTE: live test still shows
assessment not populating — see HANDOFF for the consolidation plan
that supersedes this.
3. Enter-to-submit: chat-input convention (Enter submits, Shift+Enter
inserts newline) on the escalate-flow forms. RichTextInput gains an
optional onSubmit prop; EscalateModal wires it to handleSubmit;
ConcludeSessionModal gets the same handler on its plain textarea.
4. PendingEscalations: each row is now expandable. Click row body to
reveal the engineer's escalation reason, step count on record,
confidence tier, and PSA ticket number. Pick Up still clicks through
directly. Single-expand-at-a-time keeps the dashboard compact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four items from the design-plan audit, all flagged as locked-design or
Codex corrections, shipped together so the GTM demo path covers them
end-to-end before bug bash.
1. Live AI assessment refresh on the magic-moment screen. Backend already
publishes handoff_assessment_ready when enrich_escalation_async commits;
wire the frontend listener so the senior sees the assessment populate
without a manual reopen. New event type + onAssessmentReady handler on
streamEscalations; AssistantChatPage opens a scoped SSE subscription
whenever it tracks a handoff missing its assessment, refetches on match,
and replaces magicHandoff / overlayHandoff in place. Closes the loop on
the async-assessment commit e8ba74e.
2. Suggested-step chips below the chat input. Locked design from the plan
(Codex correction). Chip strip renders above the composer post-claim
when ai_assessment_data.suggested_steps[] is non-empty. Click prefills
the input and focuses; first send or explicit X hides for the session.
3. Unread 6px dot on EscalationQueue cards. localStorage-persisted seen
set (rf-escalation-seen, capped 200). Dot top-right when not seen.
Cleared on open (card click) or claim (Pick Up) — NOT on hover, per
Codex correction. Pick Up stops propagation so it doesn't double-fire.
4. Race-condition toast on claim conflict. The /claim endpoint previously
silently overwrote claimed_by — both seniors thought they owned the
session. New HandoffAlreadyClaimedError carries the winner's id/name/
timestamp; claim_session rejects different-user re-claims (same-user is
idempotent for double-click safety); endpoint returns 409 with
structured detail. AssistantChatPage.handleStartHere extracts and
surfaces "Already claimed by {name} {time_ago}." via toast, drops
?pickup=true, dismisses magic-moment so the loser flows back to queue.
Tests: 2 new unit tests in test_handoff_manager.py (conflict raises,
same-user idempotent). Full handoff + escalation suite (34 tests) green.
Frontend tsc -b clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three improvements driven by live wedge testing.
1) Notification title now includes a problem snippet and PSA ticket
suffix when present:
"Escalation from Jane · #12345: Outlook is failing to sync email…"
Replaces the prior "Session escalated by Jane" copy that made every
escalation from the same junior look identical in the bell panel.
Snippet is trimmed to 70 chars with ellipsis. handoff_manager now
passes psa_ticket_id through in the notify() payload so this works
for both /escalate and /handoff entry points.
2) AI enrichment (assessment + enhanced escalation_package) moved to
a FastAPI BackgroundTask. The escalating engineer no longer waits
on 15-25s of Sonnet latency — handoff creation returns as soon as
snapshot, status flip, dual-write, documentation, PSA push, and
notify() are committed. enrich_escalation_async opens its own DB
session, runs both AI calls, updates handoff.ai_assessment +
session.escalation_package, commits, and publishes a new
`handoff_assessment_ready` event on the escalation bus. Frontend
doesn't yet listen for that event — the magic-moment screen still
shows a placeholder ("AI assessment is still generating. Reopen
this view in a few seconds…") which is honest about the state.
Live polling / auto-refresh on the bus event is the natural next
step.
3) ChatSidebar entries now surface the problem summary as a secondary
line and tag PSA-linked sessions with a monospace #ticket badge plus
an "Escalated" pill on in-transit sessions. ChatListItem grew
problem_summary, psa_ticket_id, and status fields; loadChats
populates them from listSessions. The user couldn't tell their own
sessions apart in the sidebar because they all rendered as "New
Chat" with no distinguishing detail — this fixes that for any
session, escalated or not.
Test plan
- Backend full suite: 1103 passed in 255.85s with -n auto.
- Frontend tsc -b clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the dedicated 4-section handoff-context view that renders BEFORE
the FlowPilot session for senior techs picking up an escalated
session, then dissolves on "Start here". This is the wedge's
demonstrable magic moment — what the GTM Loom records.
- HandoffContextScreen.tsx: pure presentational, takes a HandoffResponse
plus onStartHere / onDismiss callbacks. Sections: header
(problem summary, domain, step count, escalated-time, priority badge),
"What's been tried" (engineer notes + step-count affordance), "AI
assessment" (likely_cause / suggested_steps / confidence badge), Start
here CTA. Confidence badge accepts both numeric (0..1) and string
("low"/"medium"/"high") shapes — backend currently emits the latter.
Renders an explicit "assessment unavailable" branch when
ai_assessment_data is null (the 5s timeout from 9bdd995 fired).
Honors prefers-reduced-motion (animate-fade-in vs animate-slide-up).
ARIA dialog + focus on the primary CTA. Esc dismisses when used as a
re-openable overlay; pre-claim, Start here is the only exit.
- FlowPilotSessionPage.tsx: on /pilot/:id?pickup=true, fetch the
handoff list via handoffsApi.listHandoffs (account-scoped via RLS,
no claim required) and find the latest unclaimed escalate handoff.
If found, render the magic-moment screen and skip the regular
loadSession (the senior isn't yet escalated_to_id, so GET would
404). Start here calls claimHandoff, drops the pickup query param,
dismisses the screen — the existing loadSession effect then fires
because the senior is now escalated_to_id. A "Context" toolbar
button on active sessions re-opens the screen as a dismissible
overlay (visible only when the senior arrived via the magic-moment
flow this session — handoff lookup on demand).
Verified end-to-end against the running dev stack: listHandoffs
returns the unclaimed handoff with full payload; claim flips session
status from escalated → active; subsequent GET succeeds. tsc -b clean.
Defers (TODO followups): suggested-step chips below the chat input
that prefill on click (requires threading through to
FlowPilotMessageBar); snapshot expansion to include the recent
diagnostic steps pre-claim; toolbar Context button on sessions where
the senior didn't arrive via magic-moment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the frontend live-arrival slice on top of the test-stabilized SSE
backend. Senior techs now see a junior's escalation slide into the
queue without refresh.
- streamEscalations(handlers, signal) in aiSessions.ts: fetch-based
ReadableStream parser (native EventSource cannot send auth headers).
Handles SSE frames, partial frames across chunks, : keepalive
heartbeats. Dispatches ready and handoff_created.
- HandoffCreatedEvent + EscalationStreamHandlers types mirror the bus
payload published by HandoffManager.dispatch_escalation_notifications.
- EscalationQueue.tsx: AbortController-managed subscription with
exponential-backoff reconnect (1s → 30s cap, attempt counter resets
on ready). On handoff_created, refetch and diff against previous IDs
via sessionsRef; new arrivals prepended (newest-first) above
established cards (oldest-first preserved). Slide-in tag held for
800ms so the locked 200ms animation completes. Tab-title flash
prefixes (N) while document.hidden, restores on focus / unmount.
prefers-reduced-motion swaps slide-in for fade-in. ARIA region +
aria-live=polite + aria-label on heading. Pick Up bumped to py-2.5
to clear the 44px touch floor.
Verified end-to-end against the running dev stack: subscriber received
the ready frame on connect; after posting a handoff via the API, the
subscriber received the handoff_created frame with the expected
payload — wire format matches the parser. Backend regression: focused
subset still 32 passed in 18.91s. Frontend tsc -b clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Surfaces the new GET /analytics/flowpilot/escalations endpoint as a card
above the EscalationQueue list. Closes the loop from yesterday's metric
endpoint commit — seniors and owners see the wedge stat the moment they
open the queue, which is the daily-reps version of the GTM ROI story.
Pieces:
- EscalationMetrics TS interface mirroring the backend Pydantic model
(incl. metric_definition disclaimer field)
- flowpilotAnalyticsApi.getEscalationMetrics(period) client method
- EscalationMetricCard component:
* loading skeleton, error state, zero-data empty state
* avg + median + n_with_action/n_claimed conversion rate
* humanized seconds → "Ns" / "N.N min" formatting
* inline disclaimer reminding callers this is in-product time-to-
first-action only, NOT the savings claim — pair with manual
baseline (per /codex review's two-metric correction)
- Wired into EscalationQueuePage above EscalationQueue
DS-aligned: card-flat, accent-dim usage held to interactive elements,
text-muted-foreground for secondary copy, font-heading on the headline
number, explicit transition properties (no `transition: all`). Respects
prefers-reduced-motion implicitly (only animation is the loading pulse,
which Tailwind's animate-pulse already gates).
tsc -b clean. No new tests in this commit — component is a thin
state-machine over an axios call; integration coverage comes from the
existing backend tests + the e2e Playwright work in the plan.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The new react-hooks lint rule "Calling setState synchronously within an
effect can trigger cascading renders" flagged real anti-patterns in
four spots. Refactored each per the rule's intent (derive during render,
or use useSyncExternalStore for external subscriptions).
1. hooks/useMediaQuery.ts — replaced the useState + useEffect pair with
useSyncExternalStore. That's the canonical React hook for
subscribing to external stores (matchMedia in this case) without
mirroring into local state via an effect. Snapshot/getServerSnapshot
pair preserves the SSR-safe behaviour.
2. components/network/nodes/DeviceNode.tsx — the prop-sync useEffect
that copied nodeData.label into labelValue was redundant.
labelValue is the EDIT BUFFER; while not editing, the displayed
span now reads nodeData.label directly. The buffer is initialized
only when an edit session starts (onDoubleClick).
3. components/network/nodes/GroupNode.tsx — same pattern, same fix.
4. components/dashboard/TicketQueue.tsx — the
setTickets([]) + setLoading(true) + fetchTickets() chain in the
effect was the cascade. Pushed those writes inside fetchTickets
(after the function boundary, so they batch with the eventual
setTickets(result)). Added a request-id ref so a slow first
response can't overwrite a fast second one.
Frontend lint: 20 errors → 0 errors. tsc -b clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five files, all stylistic:
- useFlowPilotSession.ts: typed the axios error shape with a narrow
inline type instead of \`as any\`.
- FlowPilotSessionPage.tsx: same — typed location.state once, then
destructured.
- ScriptBuilderTab.tsx: handleViewScript was a placeholder no-op;
declared the args properly with \`void script; void filename\` so the
signature matches ScriptBuilderChatProps without no-unused-vars
firing.
- TicketsPage.tsx: replaced 8 ternaries-as-statements (\`x ? f() : g()\`)
with proper if/else blocks. Same control flow, satisfies
no-unused-expressions, and reads better in the URL-param update paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
react-refresh/only-export-components fires when a file with the
\`router\` const export also defines a component (the redirect helper).
Moves the small helper to its own file under components/routing/ so
HMR can keep the route-component module hot-reload-eligible.
No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings in PR #141 (PSA ticket management) so FlowPilot can ship on top
of a unified main. Two manual conflict resolutions:
1. CLAUDE.md — kept the FlowPilot ai-handoff rewrite (`.ai/`-driven
protocol). The pre-rewrite reference content (CW integration notes,
lessons archive, env vars table) lives in `docs/connectwise/`,
`docs/LESSONS-ARCHIVE.md`, and DEV-ENV.md by design.
2. frontend/src/pages/AssistantChatPage.tsx — both conflict regions
were purely additive. Concatenated FlowPilot's Phase 2-9 state hooks
(facts, activeFix, preview*, scriptPanelOpen, templatizeQueue) with
PSA's spin-off ticket state (linkedTicket, showNewTicket, spinOffHint).
Both modal mounts (TemplatizePrompt, ShortcutsHelpOverlay,
NewTicketModal) kept. All setters wired by either branch are intact.
Verification:
- `tsc -b` clean across the merged tree.
- Browser smoke-test (Session B fixture): Phase 9 ProposalBanner
("Run AI-drafted PowerShell to recover SSL VPN") renders alongside
PSA's new Tickets sidebar icon. Console clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1. EscalateInterceptDialog clipped off-screen.
The dialog was positioned with `absolute bottom-full mb-2 left-0`
under the assumption the Escalate button would have room above it.
In practice the button lives in the chat-page action bar near
y≈105, so the 302 px dialog overflows the top of the viewport
and only the last option is visible.
Switch to `top-full mt-2 right-0` — anchors the dialog below the
button and aligns its right edge with the button (avoids overflow
off the right when the button is in the right-side action cluster).
2. TemplateMatchPanel never renders on a fresh session.
`handleApplyFix` for the script_template_id branch only sets
`scriptPanelOpen=true`, but TemplateMatchPanel is mounted inside
`TaskLane.bottomSlot`. On sessions with no questions/facts the
lane defaults closed, so the panel exists in the React tree but
inside an unrendered TaskLane — the user clicks Apply fix and
nothing visibly changes.
Fix: also `setShowTaskLane(true)` in that branch so the lane
opens alongside the panel. The ai_drafted_script branch is fine
(InlineNoTemplateDialog renders in the chat region, not in the
lane), so it's left alone.
Both bugs were latent — they only surface on sessions that haven't
accumulated TaskLane state yet (questions/facts). Fresh sessions
created from the StartSessionInput hide them because the AI's first
turn populates questions and the lane auto-opens. Caught using the
new seed_phase9_qa_fixtures.py harness.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ResolutionNotePreview popover renders inside TaskLane's
overflow-y-auto region at the bottom of the lane. On a 720px
viewport with the default question/check list expanded, the
popover lands below the visible scroll position — the engineer
clicks "Preview Resolve note", sees the button label flip to
"Showing", but no preview appears on screen.
Add a useEffect that calls scrollIntoView({block: 'nearest'}) on
the popover's outer div whenever `open` flips to true. block:
'nearest' scrolls just enough to make it visible without yanking
the lane to the top.
Discovered during Phase 9 QA. Reproduced at 1280x720; fix verified
visually in the same QA run (screenshots in
.gstack/qa-reports/phase9-*/).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses docs/FlowAssist_Migration/Issues/phase-9-review-issues.md.
Issue #1 (High): "Applied partially" from the escalation intercept silently
dropped because the backend requires notes on applied_partial and the dialog
sent none. The catch was silent and the UI advanced into the conclude flow
as if the outcome were recorded.
- EscalateInterceptDialog now has a two-step flow: clicking the partial
choice reveals a notes textarea (autofocused, required non-empty) plus
Back / "Record partial & escalate" buttons.
- onChoose signature extended to (choice, notes?).
- handleInterceptChoice passes notes to patchOutcome; on failure it
surfaces a toast and does NOT advance to the conclude modal, so the
intercept stays open for retry.
Issue #2 (Medium/High): ScriptBuilderTab kept local state across active-fix
changes within the same pilot session, so a stale draft could PATCH against
a newer fix.id. Added key={activeFix.id} on the mount — forces a clean
remount per fix; backend get-or-create (keyed on user+ai_session_id) still
returns the same session row, which is the intended resume-on-refresh
semantic; but messages/editorBuffer/latestScript local state resets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the gap Phase 8 final review flagged. When a fix is in
applied_partial state and the engineer escalates, the intercept no
longer forces them to approximate with didn't-work/worked/never-applied.
AssistantChatPage's handleInterceptChoice (Task 13) already dispatches
to patchOutcome for any FixOutcome value, so no handler change is
needed — the type already supports applied_partial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate and Copy alone don't declare a run — the engineer can walk
away after copying. Phase 9 §5 defines an explicit run-declaration
affordance so applied_at only stamps on the engineer's positive
commitment. Wiring from AssistantChatPage lands in Task 13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slide-up wrapper around the existing NoTemplateDialog for rendering
in the chat region above the composer (parallel to ProposalBanner).
The chat region's width lets grid-cols-3 finally work as intended.
No change to NoTemplateDialog itself; decision callbacks and card
copy stay identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owns the inline Script Builder session lifecycle:
- Get-or-create (origin='pilot_inline', ai_session_id) on mount.
- Renders ScriptBuilderChat in AI mode and CodeModeEditor (Monaco) in
'Write it myself' mode. Mode toggles via display:none so buffer and
messages persist across switches.
- Submit → sessionSuggestedFixesApi.patchScript; emits onScriptDrafted
to parent, which refreshes the fix and hides the tab strip.
- Relays in-progress state to the parent via onProgressChange for the
ChatTabStrip's indicator dot.
ScriptBuilderChat is untouched (stays presentational). Persistence
semantics live on the controller, not the display component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-tab strip for the chat region. Parent controls mounting (strip only
appears when the fix needs a script drafted). Indicator dot signals
in-progress draft state. Tab switching via onChange callback; parent
handles display:none toggling so tab contents preserve state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Superseded by ProposalBanner (Phase 8). The import was already removed
from AssistantChatPage in the previous commit; this deletes the orphaned
file itself and strips the now-unused suggestedFixSlot prop from
TaskLane's interface and both call sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the task-lane SuggestedFix card with the ProposalBanner docked
above the chat composer. Wires:
- Resolve-while-verifying auto-marks applied_success (one-click resolve).
- Escalate-while-verifying opens EscalateInterceptDialog to capture the
real outcome (default: didn't work) before handoff.
- 3+ post-apply engineer messages trigger the passive Nudge banner.
- AI [FIX_OUTCOME] proposals surface in the AIConfirming state; one-click
confirm applies the outcome.
Banner state resets on session switch via resetSessionDerivedState.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anchored above the Escalate button, captures fix outcome before the
engineer hands off the ticket. Defaults to 'didn't work' on Enter
(the common case). Alternatives: 'worked, escalating for another
reason' (preserves success) and 'never actually applied' (dismiss).
Task 11 will wire this to AssistantChatPage's Escalate handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes ProposalBanner's state machine. AIConfirming (accent-blue)
surfaces the AI's [FIX_OUTCOME] proposal with one-click accept; Nudge
is the compact passive-prompt variant for post-apply chats; Collapsed
is the 28px expand-hint strip.
Adds onSilenceNudge prop so the parent can silence the nudge without
collapsing it (Task 11 wires this). Removes the last three stale
eslint-disable-next-line comments — all sub-components now use props.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verifying: amber pulse animation, confidence pill becomes 'Applied Xm ago',
three actions (overflow for Mark partial, Didn't work, It worked). window.prompt
used for the partial notes + failure reason inputs — good-enough v1 pending
an inline composer.
Partial: cyan-toned to signal 'parked, outcome unknown', shows saved notes
inline, Finish it / Didn't work / It worked actions.
Adds pulse-amber to @theme animations alongside slide-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New component that will replace the task-lane SuggestedFix card. Docks
above the chat composer with a 320ms slide-up animation. This commit
implements only the Proposed state (Tasks 8 & 9 fill Verifying, Partial,
AI-confirming, Nudge, Collapsed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each lane section (What we know, Questions, Diagnostic Checks, Suggested
fix) had its own `position: sticky; top: 0` header. As the engineer
scrolled past a section, that section's header would pin until the
section's bottom edge cleared the viewport, producing an "orphaned"
label floating over unrelated content below. Headers now scroll with
their content — in a 340px-wide lane the affordance was negative value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- WhatWeKnow shows a "synthesizing" indicator + skeleton pulse while the
chat cycle is in-flight; task-lane header mirrors the signal with a
"thinking" pip so engineers know the AI is still working.
- Quiet-state hint when the lane is open (facts exist) but no open
questions, checks, or active fix — keeps the surface from looking
"finished" when the AI is about to follow up.
- Keyboard shortcuts: ⌘↵/Ctrl+↵ send in the composer (plain Enter still
sends), ⌘G toggles the Script Generator panel for the active fix,
`?` opens a new ShortcutsHelpOverlay listing all bindings. ⌘K palette
was already wired in TopBar.
- Responsive: below 1200px the task lane collapses to a bottom drawer
with a backdrop + a floating "Tasks ●" toggle button. TaskLane now
takes a `variant: 'side' | 'drawer'` prop; drawer variant drops the
resize handle and uses the shared slide-in-bottom animation.
- Build hygiene: fixed a pre-existing TS error in confirm-post error
handling (duplicate `response` type keys) and an unused-import warning
in TemplatizePrompt.
Verified: `npx tsc -b` and `npm run build` both clean against the dev
stack; Vite HMR applied each change without errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on the Phase 5 "Run now, templatize after resolve" path.
After a session resolves, drafts queued by the three-option dialog surface
as a modal that lets the engineer review the AI-proposed parameterization
and either save as a reusable team template or skip. A "don't ask again"
toggle writes to account_settings.preferences so the next resolve won't
pop the modal.
Backend:
- /api/v1/draft-templates:
* GET — list account drafts (pending_only default true; pass false for
audit view including accepted/rejected)
* GET /{id} — single draft
* POST /{id}/accept — promotes to a new script_templates row with
source_session_id / source_user_id / source_ticket_ref populated
(drives the Script Library "generated from CW #X · resolved by Y"
provenance chip). Draft flips to status=accepted,
promoted_template_id set, resolved_at stamped. 409 on re-accept /
already-rejected. 400 on unknown category_id.
* POST /{id}/reject — flips to status=rejected. 409 on re-reject.
- /api/v1/accounts/me/preferences (GET/PATCH) — thin wrapper over
AccountSettings.get_setting/set_setting. PATCH merges keys into the
JSONB column, preserving existing keys the client didn't touch.
Used by the "Don't ask again for this team" checkbox
(templatize_prompt_enabled=false) and, forward-looking, by
cw_resolved_status_id / cw_escalated_status_id from Phase 4.
- 13 tests: list filter, accept with/without edited_body, provenance
copy-through, reject, 409 on re-accept / re-reject, 400 on unknown
category, prefs round-trip with merge semantics.
Frontend:
- src/components/pilot/script/TemplatizePrompt.tsx — modal showing the
drafted script with proposed parameters in the Phase 5
ParameterizationPreview, editable name/category/description, an
individual-parameter remove button, and the "don't ask again" opt-out.
Accept posts to /draft-templates/{id}/accept + optionally PATCHes
preferences. Skip posts /reject.
- src/api/draftTemplates.ts — typed client plus accountPreferencesApi.
- AssistantChatPage: after a successful Resolve (external OR local),
fetches preferences + pending drafts for the session and queues the
modal one draft at a time. Escalate does not trigger this flow.
- Sidebar: Scripts nav shows the pending-draft count as a badge. Fetched
independently of the main sidebar stats so endpoint flakes don't
break the rest of the sidebar.
Verified live 2026-04-22: seed two drafts → GET sees both pending →
accept draft A (template created, provenance CW #99123 populated) →
reject draft B → pending count drops → PATCH opt-out → GET confirms
persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes from the Phase 5 shakedown:
1. Stale lane data leaking across chats. handleNewChat, sendPrefill, and
handleResumeNew were each missed when Phase 3/5 added activeFix,
previewKind, previewData, and scriptPanelOpen — only selectChat reset
the full set. Result: starting a new chat while a Suggested Fix card
was active showed the previous session's fix card (and any open
preview/script panel) until the next backend refresh swept it.
Consolidated all four entry points behind a single
resetSessionDerivedState() helper so adding new lane state in future
phases only requires touching one place.
2. CommandPalette TDZ on cold load. SCRIPTS_INLINE_QUICK_ACTION (line 66)
referenced PILOT_INLINE_SCRIPT_PATH declared at line 94 — module-level
evaluation hit the use before the declaration. Browser blanked with
"Cannot access 'PILOT_INLINE_SCRIPT_PATH' before initialization".
Moved the path const above its first use; also extracted
PILOT_INLINE_SCRIPT_EVENT into a tiny @/lib/pilotEvents module so
AssistantChatPage doesn't import the palette component just to read a
string — that mixed-export pattern broke Fast Refresh ("consistent
components exports") and added an unnecessary import edge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the SuggestedFix card to an inline panel that handles both cases:
template-matched fixes open the Script Library generator with parameters
pre-filled from session context; un-matched fixes open the three-option
dialog (one_off / draft_template / build_template). The decision endpoint
records the path choice with side effects: draft_template persists a
draft_templates row via a Sonnet-driven TemplateExtractionService;
build_template returns a redirect to the Script Builder; one_off just
records the choice.
Backend:
- TemplateExtractionService: drafts a parameter schema from a concrete
rendered script. Conservative by default ("prefer fewer parameters").
Round-trip-validates that templated_body only references declared
parameters; missing-key mismatch falls back to the original script
with no params. LLM/parse failures fall back identically — the
engineer can still create a draft and refine in the post-resolve
prompt (Phase 6).
- /suggested-fixes/{fix_id}/decision side effects:
* one_off → returns rendered_script (engineer's edited version or the
fix's ai_drafted_script verbatim)
* draft_template → same + creates draft_templates row with extracted
params, returns draft_template_id
* build_template → returns redirect_path=/scripts/builder?from_session=
&fix= so the frontend can navigate to the builder pre-loaded
- 400 when a non-template fix has no ai_drafted_script (template-matched
fixes take the dedicated /scripts/generate path, not this endpoint).
- 12 tests: TemplateExtractionService parse + fallback paths, all four
decision branches, edited_script override, missing-script 400.
Frontend:
- src/components/pilot/script/{TemplateMatchPanel, NoTemplateDialog,
ParameterizationPreview}.tsx — inline panels rendered in the task
lane's bottom slot when the engineer clicks a SuggestedFix card.
- TemplateMatchPanel: loads template via /scripts/templates/{id},
pre-fills params from fix.ai_drafted_parameters with cyan "from
session" tags, generates via existing /scripts/generate (already
bumps state_version on ai_session_id from Phase 3). 404 falls back
with a clear message instead of erroring.
- NoTemplateDialog: shows the AI-drafted script with proposed parameter
values highlighted in amber via ParameterizationPreview; three option
cards with the middle (draft_template) flagged Recommended; inline
edit on the script body before deciding.
- SuggestedFix card now clickable: onActivate toggles the inline panel.
- AssistantChatPage: scriptPanelOpen state + handleScriptDecision that
navigates on build_template and toasts on the other paths. Active fix
changes auto-close the panel so engineers don't act on stale state.
- Cmd+K → "Open inline Script Generator" palette entry surfaces only on
/pilot/:id routes; fires a window event the chat page subscribes to.
No Resolve shortcut added per Section 14 decision (browser ⌘R conflict).
Verified 2026-04-22 against the dev stack:
- one_off / draft_template / build_template all return the right shape
with real Sonnet TemplateExtractionService for the draft path.
- Conservative extraction confirmed: cmdkey + Restart-Process script
yielded zero proposed parameters as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the preview popover's Confirm & post action to ConnectWise (and,
via the provider pattern, any future PSA). Adds the parallel Escalate
flow with the handoff-oriented five-section markdown. Sessions without a
linked PSA ticket resolve/escalate locally — markdown stored, status
flipped, nothing posted externally.
Backend:
- EscalationPackageGeneratorService: Sonnet, five sections (Problem /
What we've confirmed / What we've tried / Current hypothesis /
Suggested next steps). Shares the preview_cache with a separate KIND
so Resolve and Escalate previews for the same state coexist.
- PSAWritebackService: post_resolution_note (RESOLUTION note type,
customer-visible), post_escalation_package (INTERNAL_ANALYSIS,
handoff for the next engineer only), transition_ticket_status with
mandatory re-fetch verification. PSAStatusVerificationError surfaces
loudly when CW silently rejects a status change — the
ConnectWise anti-pattern CLAUDE.md flags.
- Endpoints:
* POST /ai-sessions/{id}/escalation-package/preview
* POST /ai-sessions/{id}/resolution-note/post
* POST /ai-sessions/{id}/escalation-package/post
Outcomes: "resolved" / "escalated" with external_id + verified status,
"resolved_local" / "escalated_local" when no PSA linked.
- Target CW status IDs live in account_settings.preferences
(cw_resolved_status_id, cw_escalated_status_id). When unset, the post
proceeds without a status transition — response includes a
status_transition_skipped_reason rather than silently erroring.
- 7 tests: local-only path, PSA happy path with verified transition,
status verification failure → 502, skipped transition when
unconfigured, 409 on already-resolved re-post, escalate parallel path,
internal-analysis note type enforced.
Frontend:
- ResolutionNotePreview now kind-parameterized ('resolve' | 'escalate')
with inline edit + Confirm & post. Preview loads from the matching
backend endpoint; posting calls the matching endpoint; outcome toast
surfaces the verified CW status or the local-only result.
- AssistantChatPage: previewKind state replaces previewOpen; two toggle
buttons (Preview Resolve note / Escalate instead) in the lane's bottom
slot. handleConfirmPost dispatches by kind.
Verified 2026-04-22:
- Local-only Resolve + Escalate round-trip against the dev stack.
- Live Sonnet escalation-package preview; cache hit on repeat call
with no state change (separate cache kind from resolution-note).
- PSA post + status-verification paths covered by mocked-provider pytest
cases. Live CW round-trip pending a test CW instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the AI-proposed resolution path and the inline preview of the
markdown that will be posted to the customer ticket on Resolve. The
preview is keyed on (session_id, ai_sessions.state_version) so back-to-
back fetches against unchanged state hit an in-process cache instead
of paying for a Sonnet call.
Backend:
- preview_cache: in-process LRU keyed on (kind, session_id, state_version).
No TTL — state_version is the source of truth. Soft-cap 5000 entries.
- unified_chat_service: [SUGGEST_FIX] parser (last-block-wins, JSON
payload, confidence clamped 0-100), supersession persistence (sets
superseded_at on prior active row), atomic state_version bump.
- ResolutionNoteGeneratorService: pulls session, facts, active fix, and
redacted script_generations into a structured input bundle for Sonnet;
produces the four-section markdown (Problem / What we confirmed /
Root cause / Resolution). Sensitive script parameters redacted via
ScriptTemplateEngine.redact_sensitive driven by the template's
parameters_schema.
- /api/v1/ai-sessions/{id}/suggested-fixes/active — 200 with the active
fix or 404.
- /api/v1/ai-sessions/{id}/suggested-fixes/{fix_id}/decision — records
one_off / draft_template / build_template / dismissed; dismiss
supersedes; bumps state_version. 409 on dismissing an already-
superseded fix.
- /api/v1/ai-sessions/{id}/resolution-note/preview — generates or returns
cached markdown; from_cache flag in payload signals cache hit.
- scripts.py POST /generate now bumps state_version on the linked
ai_session_id when present (third source of preview-cache invalidation
per Section 5.5).
- ASSISTANT_SYSTEM_PROMPT documents [SUGGEST_FIX] (when to/not to emit,
format, supersession semantics).
- 12 tests covering the parser (well-formed, last-wins, malformed,
confidence clamping), supersession + state_version invariant, all
decision branches, preview cache hit-on-no-change + miss-after-write.
Frontend:
- src/components/pilot/sections/SuggestedFix.tsx — amber-accented card
with confidence badge; dismiss action wired to the decision endpoint.
- src/components/pilot/ResolutionNotePreview.tsx — popover with refresh,
loading state, cached/fresh indicator, ticket-ref display.
- src/api/sessionSuggestedFixes.ts — typed client; getActive normalizes
404 to null so callers don't have to special-case.
- TaskLane gains suggestedFixSlot + bottomSlot props (rendered after
Diagnostic Checks; bottomSlot anchors the Resolve action).
- AssistantChatPage: refreshSessionDerived helper batches fact + fix
refresh; fact mutations and chat sends both schedule a 500ms-debounced
preview refresh per the Section 5.5 spec.
Verified end-to-end against the dev stack with a real Sonnet call:
- /active 404 → fact create → preview generates four-section markdown
grounded only in provided facts → second preview call hits cache
(from_cache=true, no LLM call) → fact write 2 → cache miss, regenerates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the load-bearing structural feature of the FlowPilot migration: a
"What we know" panel that holds confirmed facts for a session, fed by AI
[PROMOTE] markers and engineer-added notes. Facts feed the resolution
note preview (Phase 3) and survive across turns via stable UUIDs assigned
to pending_task_lane items.
Backend:
- FactSynthesisService: create/update/soft-delete facts with atomic
state_version bumps; LLM-backed synthesize_from_question/check on the
fact_synthesis (Haiku) action tier per Section 6.6.
- /api/v1/ai-sessions/{id}/facts CRUD + /facts/promote (proposed_text or
via synthesis). PATCH returns 403 for question/diagnostic_check facts
(edit the source item instead, Section 7.3).
- unified_chat_service: [PROMOTE] marker parser (JSON-block per Section
8.1 spec drift note), stable-UUID assignment for pending_task_lane
questions/actions preserved by exact text/label match across turns.
- ASSISTANT_SYSTEM_PROMPT: documents [PROMOTE] format, when to/not to
emit, hallucination guardrails, source_ref handling.
- 17 tests covering parser, stable IDs, service validation, CRUD,
editability rule, both promote modes, 422 null-synthesis path,
state_version invariant.
Frontend:
- src/components/pilot/sections/{WhatWeKnow,WhatWeKnowItem,AddNoteButton}
— green-gradient section above Questions, dashed-circle check, inline
edit/delete gated by the server's editable flag.
- TaskLane gains a whatWeKnowSlot prop (existing assistant/ folder kept
per the doc's "rename is opportunistic" guidance).
- AssistantChatPage fetches facts on selectChat and refetches after each
chat send (so [PROMOTE]-synthesized facts appear immediately); auto-
opens the lane when facts exist.
Verification: end-to-end smoke against the local docker stack confirms
all five endpoints (list/create/patch/delete/promote) plus the 403
editability rule. pytest suite verifies the same with mocked LLM. Live
[PROMOTE] flow remains untested until used in the UI — the marker shape
is covered by parser tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses the pre-existing dual-surface setup (AssistantChatPage at /assistant,
FlowPilotSessionPage at /pilot) into a single chat-primary surface per
architectural claim #1 of FLOWPILOT-MIGRATION.md.
Router changes (frontend/src/router.tsx):
- /pilot and /pilot/:sessionId now render AssistantChatPage.
- /assistant redirects permanently to /pilot via <Navigate replace>.
- /assistant/:sessionId redirects to /pilot/:sessionId preserving the ID
via an AssistantSessionRedirect helper that reads the param.
- FlowPilotSessionPage is no longer imported or mounted. Per the
beta-history-disposable decision, the file stays on disk for reference
but is unreachable; delete once nothing else in the tree imports it.
Dispatcher de-branching — previously these sites routed by session_type
(chat -> /assistant, otherwise -> /pilot). All now unconditionally go to
/pilot/:id since session_type is no longer used for frontend routing:
- components/dashboard/ActiveFlowPilotSessions.tsx
- components/dashboard/RecentFlowPilotSessions.tsx
- components/flowpilot/AISessionListItem.tsx
(keeps isChat for icon selection, but linkTo is unconditional)
User-facing label + navigation updates:
- components/layout/CommandPalette.tsx: "AI Assistant" palette entry
becomes "FlowPilot" pointing to /pilot; the sparkles quick-action also
routes to /pilot.
- components/dashboard/StartSessionInput.tsx: both navigate() call sites
now go to /pilot instead of /assistant.
- lib/routePrefetch.ts: prefetch entry for AssistantChatPage keyed to
/pilot (the real surface) rather than /assistant (now redirect-only).
Preserved intentionally (not user-facing routes):
- Backend /assistant/retention API path and the assistantChatApi module
name — those are internal API and module identifiers, not SPA routes.
- src/components/assistant/* and src/types/assistant-chat — TypeScript
module paths, not routes.
- Sidebar.tsx — no top-level AI entry existed to rename; /pilot is
already in the History group's matchPaths. Whether FlowPilot deserves
its own rail entry is a future UX decision, not Phase 1 scope.
- FlowPilotAnalyticsPage at /analytics/flowpilot — analytics for the
unified product, not guided-only, per the agreed Q16 interpretation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>