Skip to content

Invoice Process: Exception UI v3#970

Draft
petervachon wants to merge 46 commits into
mainfrom
invoice-exception-v3
Draft

Invoice Process: Exception UI v3#970
petervachon wants to merge 46 commits into
mainfrom
invoice-exception-v3

Conversation

@petervachon

Copy link
Copy Markdown
Collaborator

Draft preview of the invoice exception review v3 UI.

Changes

  • Header disposition split button with gated approve (store-layer guard via approveGated)
  • Wait and send-back disposition actions wired to runtime
  • Suggested fix feedback loop with two-step flow (positive + correction paths)
  • Correction path triggers 2s regen interval with interim loading state and seeded prose
  • Primary-filled split button on PO action, footer legal line with info icon

🤖 Generated with Claude Code

petervachon and others added 30 commits July 24, 2026 07:45
Invoice review template, demo API routes, and the Slack escalation server.
Rebased onto latest main; prior prototype history (8 commits, incl. a merge)
squashed into a single commit.

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

Add the "next" exception-review experience, toggled against "current" via a
dev switch in the profile menu (InvoiceVersionProvider + ShellProfileExtras).

The center panel reads as a timeline: collapsed agent history, the live
exception, and any checks gated behind it. Confident fixes surface in a
suggested-fix card (reasoning + apply/alternative) but never auto-resolve.
Invoice-level decisions (Approve/Reject/Hold/Flag) move to a header
split-button. The right panel drops the Activity tab and merges details.

All agent behavior is stubbed; single-exception and cascade invoices render
through the same flow.

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

Rework the next invoice-review workspace around multiple exceptions per invoice
and one source of truth across surfaces.

- Data: exceptions[] with a canonical exception dictionary (label + tone per
  type), assignee/status on InvoiceReview, getExceptionSummary/openExceptions;
  fixtures converted (INV-84471 loop hero, INV-60118 multi-open, high-value as
  a review reason). revalidateException returns cleared/surfaced.
- Shared per-invoice runtime store (InvoiceRuntimeProvider) so the workspace,
  queue rail, and table reflect resolution live from the same record.
- Queue rail derives by assignee filter with a live +N suffix and "Ready to
  approve"; table exception cell shows lead + N (tooltip) or "Cleared".
- Center column: stable exception index (master-detail) with ordinals and a
  "Viewing" indicator; stage crossfade + staggered entrance.
- Phased resolve choreography (confirm -> check -> reveal -> commit): the
  resolved block collapses in place, nothing inserts above the live exception,
  and the next exception/surfaced items appear only after re-validation settles.
  Reduced-motion aware.
- Stage headline wraps (no ellipsis); resolved marker uses user-round-check.

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

Build out the invoice-review resolve flow and add the completion end state.

- Completion moment: at zero open exceptions the resolve log compresses into a
  peek and a terminal block appears (solid-success marker, summary built from
  resolution shortLabels, Approve invoice / Hold wired to the header handlers).
- Phased resolve choreography kept, with a cleaner Phase 1 collapse (content
  fades before the height animates) and no scope labels on the live stage.
- Resolution data propagation: a resolution dataPatch (e.g. Link PO-5123)
  updates the shared record via the runtime store; header PO pill and Details
  Purchase order reflect it.
- Chat-style scroll follow: after a resolve commits (or on selection), the
  column scrolls so the live block's top lands ~24px below the header, clamping
  to the bottom when the rest fits; user scroll cancels the follow; reduced
  motion applies instantly.
- Header scrim (fade + blur, tunable --header-scrim-h) over the center column.
- "Issue x of x" moved beside "Needs your decision" with a subtle divider.
- Split-button: one primary color with a primary-600 rule between the halves.
- Copy sweep: removed em-dashes from fixture and UI strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the layout versions to v1 (was current), v2 (was next), and add v3;
migrate the stored preference. v2 keeps the bordered exception index; v3
previews an "Up next" strip in its place, selected via an exceptionListVariant
prop so the two can be compared from the Layout menu.

The strip lists only open, non-active exceptions in standing order: borderless,
one quiet line each (tone dot + headline + scope + optional New tag), no
ordinals, chips, or "Viewing". Lines pull an exception forward on click
(keyboard-focusable); it updates only at the Phase 3 commit. The index
component is kept intact behind the flag.

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

Make every resolved timeline row expand to its original exception (lossless
history) and unify the whole exception column onto one shared row anatomy.

- Expandable resolved rows: click unfolds a read-only, past-tense render of the
  original exception (chip, muted headline, finding, quiet fix shell with a
  resolution stamp) inline and inside the completion peek. Per-row local state,
  survives resolve commits, respects prefers-reduced-motion.
- One row anatomy (EventRowBody) for flat, expandable, peek, agent-step, and
  ResolvingBlock rows: title 14/500 primary, sub secondary, right-aligned
  tabular-nums time column, fixed chevron slot, center-aligned.
- Rhythm tokens: 12px nested peek / 20px events / 28px section transitions;
  middot separator grammar replaces the pipe in the group header.
- Scroll-to-latest pill: reveals past 160px from the live edge, re-anchors and
  re-engages auto-follow on click, dot flags events committed while scrolled
  away. Auto-follow rules, choreography, and header scrim unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Routing an exception no longer marks it resolved. It parks the exception in a
waiting state, blocks the disposition, and stubs the corrected-invoice return.

- Data/runtime: `waiting` status + WaitingRef, park/unpark in the runtime store,
  openExceptions/getExceptionSummary exclude waiting and report waitingCount.
  Seams: sendSupplierEmail (outbound), receiveCorrectedInvoice (return).
- Timeline: route choreography (collapse to a waiting row, no re-check), info
  waiting/waiting-complete markers, counter "+ N waiting", and a waiting
  terminal that blocks Approve. Dev `?dev=1` simulates the corrected invoice to
  close the loop.
- Supplier email: clicking "Ask supplier" opens a draft modal (ported from the
  v1 EmailPanelTab into a prop-driven next/ component) as the confirmation step;
  Send commits the park with the drafted email. Internal routes park directly.
  The sent email is the audit artifact: recipient in the waiting sub, a
  read-only subject/body block in the expanded row and the terminal request
  card, and a Follow up flow that appends a "Followed up" event.
- Header/queue: Approve disabled with a "Waiting on {who}" tooltip while
  waiting; a derived "Waiting" queue group with an info chip.
- Waiting terminal elevation: per-exception request card (what was sent, the
  artifact, the response-time clock) + Follow up / Hold invoice actions.
- Polish: single-size-per-line right-aligned tabular timestamps, three-tier
  rhythm, scroll-to-latest pill, lossless expandable rows, mid-sentence
  lowercased "just now", zero-count clauses dropped from the history label,
  bold active-step titles (focus + terminals), and a type-drift guard
  (scripts/check-type-drift.mjs) wired into lint.

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

Approve and Hold become real dispositions, and the whole timeline event log
moves into the runtime store so history and disposition survive remounts
(freeze/restore leans on that).

- Disposition: runtime is the single source in v2/v3 (approved | held); template
  completionMap/parkedMap are not consulted for approve/hold. Approve is a hard
  commit with a stamped terminal; Hold is a reversible overlay with a reason
  popover and Resume; both flow through seams (approveInvoice/holdInvoice/
  resumeInvoice). Header chip + Approve gating derive from disposition; overflow
  Hold removed (terminal buttons are the only Hold entry). Queue gains "On hold"
  and "Approved" groups.
- Event log: RunEvent relocated to the data layer; the log lives in the runtime
  store, appended atomically inside each mutation (commitResolve / parkException
  / appendEvents / setDisposition) with batch-safe keys, so it never half-persists
  or double-fires. Summary and history label derive from it.
- Fixes: Auto-approved (non-review) cards are non-interactive by data presence,
  and all three lookups (timeline / header-details / pager) show honest absence
  instead of substituting another invoice. Restore the "corrected invoice
  cleared" summary clause. Stop lowercasing exception labels in the default
  shortLabel so "PO"/"VAT" survive; give outside-po-period a resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the invoice-review commit gates onto one shared ReasonDialog
(reason chips + optional note) and restructure the queue rail as a status
filter.

- ReasonDialog: shared Dialog-primitive component (title/description/chips/
  note/footer), controlled or trigger-driven; used by Hold, Route, Reject.
- Route-to-owner: confirm dialog with named owners (retires bare role
  strings), reason/note carried on the waiting event and shown in history.
- Header overflow: Reject + Hold open dialogs (Hold uses the runtime
  disposition, Reject destructive); Flag removed until it can park a
  resumable state.
- Disposition events carry an explicit actor so reviewer actions use the
  person marker, not the AI sparkle; held summary regains its sentence break.
- Held terminal hoisted so a header Hold is resumable mid-review.
- Queue rail: status-filter tabs (All/Waiting/On hold/Done) sorted by due
  date under date section labels; flat neutral active tab on a muted track.
- SupplierEmailModal title/description align to the Dialog primitives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring Reject to full parity with Approve/Hold, and add a "show in source"
inspect affordance on exception evidence.

Reject to disposition parity:
- InvoiceDisposition gains "rejected" (permanent, no undo); buildReject +
  rejectInvoice seam; receiveCorrectedInvoice no-ops for rejected.
- Header Reject commits via the runtime disposition (atomic event, no toast),
  destructive confirm; a permanent disposition disables Approve + overflow.
- Rejected terminal (solid destructive marker, no actions, finality copy)
  supersedes any live/waiting/held/passed state; history stays expandable.
- Queue folds rejected into the Done tab; the list table now reflects runtime
  approve/reject/hold so the Approved badge shows the success token live.

Highlight-in-source:
- InvoiceException.sourceAnchors seam (anchor id = doc-region contract) +
  runtime showInSource(nonce) channel; anchors on VAT, price/total, invoice
  date fixtures.
- Highlighter trigger rides the ON INVOICE evidence value (not the action
  row); click switches to the Source tab, smooth-scrolls the first region to
  the upper third, and settles an amber fill + ring (same warning token as the
  chip). Live-exception only; clears on resolve/route/terminal.
- Reduced motion: instant scroll, no settle, no elevated frame (synchronous
  matchMedia + motion-reduce backstop).

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

Left panel:
- Rename the queue heading to "My Work".
- Add a "My Work" nav item that opens the first Due-today invoice's detail;
  "Invoices" returns to the list. Each invoice now has a shareable, bookmarkable
  URL (?invoice=INV-XXXX) that updates as you navigate and deep-links on load.

Exception timeline:
- Approved terminal now mirrors the rejected terminal: solid green check +
  "Approved" headline + amount/vendor/time stamp + "Sent for payment.". "All
  checks passed" demotes to a history row (sparkle marker) with the resolution
  summary; the stale "ready for your decision" line is gone.
- Approve plays the full re-check choreography (confirm -> check -> reveal)
  before revealing the terminal; hold/reject stay snappy. Reduced motion reveals
  instantly.
- Removed the static "Validation runs again after each fix" explainer row.
- Rail line signals state with no words: a short fading tail on open states
  (live/waiting/held/decision-pending), a hard stop on terminals (approved/
  rejected). On the live decision node the tail grows down and fades toward the
  suggested fix card.
- Waiting/hold Hold buttons use the secondary variant; tightened peek spacing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesigns the Up Next strip to show findings at decision granularity:
grouped rows (type + count badge) when multiple findings share a type,
single enriched rows (synthesis text + muted locator) otherwise.

- buildQueueRows groups waiting exceptions by type; groups of 1 render
  as single rows with synthesis ≤45 chars, falling back to the type label
- Header shows "Up next · N findings" only when at least one group row
  exists (compression is present); plain "Up next" otherwise
- Active exception excluded from Up Next items before grouping
- New ExceptionTypes: goods-not-received, qty-over-invoiced with EXCEPTION_META
- Adds INV-30291 (CDW Netherlands, grouped state, 8 findings) and
  INV-30292 (Falcon Procurement, hybrid state, 4 findings) with full
  detailDataMap entries and rootCause seam on INV-30291
- Synthesis sweep: all seeded exceptions now carry ≤45 char synthesis
  strings; INV-30292 line 4 price-mismatch intentionally omitted as the
  live fallback demo
- Hold until receipt / Route to data owner on INV-30291 gnr suggestions;
  suggestionLabel("wait") reads data.label instead of hardcoded string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ive Up Next

Replaces the Up Next queue (active excluded, rows disappear) with a stable
Findings map: all non-waiting findings listed in fixed escalation order, one
row per type-group, cursor marks the current row in place.

Row states (no reordering, no removal):
- Pending individual/group: amber dot, synthesis/type label, count badge,
  chevron, ~120ms hover. Click moves cursor to that row.
- Current individual: slightly larger filled amber dot, 500-weight text,
  hairline ring + muted surface, right-aligned CURRENT micro-label.
- Current group: same position, type+locator, progress badge (X of N),
  CURRENT label, raised surface. Cursor advances as members resolve.
- Resolved individual/group: muted check icon, muted text, no chevron,
  not clickable. Resolved group reads "type · N resolved".

Header: FINDINGS · N until first resolve; FINDINGS · R OF N RESOLVED after.
Section hides when only one finding total (single-finding invoices).

Cursor mechanics: exactly one current row at all times; clicking non-pending
is a no-op (fixes the oscillation bug where double-click toggled two issues).
Stepper counter uses total non-waiting findings as M (stable denominator).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pletion pill

Seed reasoning bodies on all INV-30291 and INV-30292 group suggestions
that were missing them (gnr-2/3, qoi-1/2/2t/3/3t, price-4), so every
SuggestedFixCard renders a rationale paragraph.

Change the current-group progress pill from position semantics
("N+1 of M") to completion semantics ("N of M done") so it reflects
resolved count only, not cursor position. Resolved group label changes
from "N resolved" to "N done" for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or-synced

Four-column table (# | ITEM | QTY / PO | AMOUNT) replaces the old list.
Billed qty shown amber/500 when it exceeds PO qty; PO qty in muted below.
Price-mismatch lines show amount amber with PO line total in muted below.
Header reads "QTY / PO" when a PO is linked, "QTY" when none (INV-60118).
Removed all ↑ qty / ↑ price badges; INV-66216 monitor arm keeps "high value".

Seeded poQty on INV-30291 (8/20/15), INV-30292 (25/30/16/50 + agreed on
line 4), INV-GRN-001 (poQty:1). Removed Missing PO flag from INV-60118.

Cursor sync: ExceptionTimeline fires runtime.setCursor on active change;
RuntimeStore gains getCursor/setCursor backed by cursorMap state.
DetailsCombinedTab reads cursor, resolves exception scope to a line number,
and applies peak→rest amber highlight (double-rAF, 600ms transition,
reduced-motion skip).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ender loop

Inline array literals in JSX create a new reference on every render. The
SidebarNav useEffect depends on navItems, so a new reference each render
caused setExpandedItems → re-render → new reference → infinite loop.

Memoize with useMemo + [] so the reference is stable for the component's
lifetime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
runtime.setCursor updates cursorMap → useMemo([map,cursorMap]) produces a
new runtime object → ExceptionTimeline's useEffect (which listed runtime as
a dep) fired again → setCursor → cursorMap changed → loop.

Two guards:
- setCursor bails out early when the stored value already equals the new one
  (returns prev so React skips the state update entirely).
- Remove runtime from the cursor-sync effect deps; the effect's trigger is
  active?.id, not the runtime reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A. Findings section:
- Remove uppercase/tracking from "Findings · N" section header — sentence case
- Remove uppercase/tracking from "Current" row caption — sentence case
- Row padding px-0.5 → px-2 so all rows share the same left text edge;
  current row inset surface is padded + rounded, not full-bleed

B. Details panel metadata — grid + top-bar dedupe:
- Remove Assignee, Currency, Due date, PO rows (all in persistent top bar)
- Replace label-left/value-right rows with a 2-col label-over-value grid
- Cluster 1: Doc date · Payment terms · VAT number · Service period (optional)
- Cluster 2: Vendor (+ email) · Bill to (+ address) — long values wrap in cell
- Drop Invoice / Parties / Reference section headers; hairline separation only
- Add servicePeriod?: string to InvoiceDetailData; seed "Apr 1–Jun 30, 2026" on INV-55832
- Line items table now starts meaningfully higher without scrolling

C. Panel default width: clamp(380, 28vw, 520) via lazy useState initializer
- ~380px at 1280px viewport (floor keeps center column comfortable)
- ~423px at 1512px (reference viewport)
- ~520px cap on wide monitors
- TypeScript: import ShellNavItem type + annotate useMemo<ShellNavItem[]>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove all CSS uppercase/tracking from user-facing labels in both the
decision card and the details panel. Labels were stored in sentence case
already; the CSS transform was the only thing making them render all-caps.

Middle surface:
- Finding card side labels (Invoice date, PO window, etc.)
- Exception metrics row (Amount, Threshold, Excess)
- "Up next" card label

Right panel:
- Details grid cluster labels (Doc date, Payment terms, VAT number,
  Service period, Vendor, Bill to)
- Line items column headers: ITEM→Item, QTY/PO→Qty/PO, AMOUNT→Amount,
  remove tracking-wide (column headers now match section-header register)
- "Sent" outbound email badge
- "AI summary" section header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add vendorAddress to InvoiceDetailData interface and all 10 seed entries
- Add subline slot to line items; replace high-value Badge with warning-colored subline text
- Remove judgment prose from linesAlert on 5 invoices; move payment-block to INV-84471 body
- Add Due date to cluster 1; render vendorAddress under vendor email (street/city split)
- Panel headings: "Invoice details" 16px bold, "Line items" 14px bold, both tracking-tight
- Two-tier metadata grid: labels 12px/muted, values 14px/medium; spacing-only separation
- Line items: fixed grid-cols-[16px_1fr_56px_80px], tabular-nums, subline slot
- Qty/PO collapse to single line (12 / 8); warning color on mismatch
- Row highlight bleeds edge-to-edge via -mx-6 px-6
- Total row: flex layout to avoid column overflow; 15px amount
- Vendor email rendered as mailto link with hover transition
- poQty sweep across INV-55832, INV-66216, INV-91003, INV-48209, INV-77294

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a full correction pipeline so fix actions and manual edits converge
on a single runtime source of truth, and auto-resolve exceptions when
the predicate registry clears them.

Section A - correction pipeline foundation:
- Add LineCorrection, DetailCorrections, PredicateBaseData types
- Add correction field to Suggestion; suggestionLabel derives copy from
  it so qty strings never hardcode what the record already knows
- Add detailCorrections to InvoiceRuntime; commitResolve merges patches
- Add RESOLUTION_PREDICATES registry with qty-over-invoiced + vat-mismatch
- Add correctDetail store mutation: merges corrections, runs predicates,
  emits auto-resolve + revalidated events atomically
- Fix INV-30291 seed to use PO quantities (10/25/20) as single source of
  truth; correction payloads derive label copy from those values
- ExceptionTimeline threads detailCorrections through resolveActive/commit
- showInSource calls onRequestEdit for registered exception types (vat)

Section B - edit mode:
- EditModeContext provides editMode, editFocusField, enter/exitEditMode
- CorrectedMark inline UserPen icon shown next to corrected field labels
- DetailsCombinedTab merges rt.detailCorrections over base data (cd/dc);
  uses correctedLines for line items table; Edit button on heading
- DetailsEditForm: controlled inputs for all extracted fields + lines,
  dirty tracking with label suffix and border highlight, field count footer
- EditModeView: absolute overlay covering center+right, mode header bar
  with Cancel and Save & re-check, 420px form + flex-1 source split,
  Esc-to-cancel with AlertDialog discard guard
- CenterPanelNext accepts onRequestEdit and passes it to ExceptionTimeline
- InvoiceDetailPane provides EditModeContext and renders EditModeView
  when editMode is true

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…phy, field sync

Section A: Route all fix actions through correctDetail (no direct resolvedIds).
- outside-po-period predicate + billing-account predicate
- INV-55832 date fix via correction.documentDateFormatted
- INV-66216 VAT verify via correction.vat
- INV-60118 account via correction.billingAccount
- correctDetail extras: surfaced/dataPatch/settledSub on same mutation
- SessionStorage persistence: corrections survive page reload

Section B: Edit mode entrance choreography.
- Overlay fades 120ms; form slides translateX 260ms ease-out
- PDF column fades + rises 8px with 100ms delay
- Exit reverses; doExit drives cancel/save/discard/Esc paths
- Loading skeleton on PDF column until pdfReady
- prefers-reduced-motion cuts all transitions

Section C: Field source sync.
- EDIT_FIELD_ANCHORS map + fieldAnchor() + fp() onFocus/onBlur
- setFieldHighlight in runtime; takes priority over exception highlight
- SourceTab: fieldHighlight overrides exceptionHighlight in activeAnchors
- data-anchor on vendor, vendorEmail, billTo, billAddress

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…settle pulses

A. INV-30291 race fix: parseInt instead of Number() for PO qty string ("10 units"
   was parsing as NaN, predicate never cleared). nextOpenId now prefers exceptions
   after the current index before wrapping, so the cursor advances forward instead
   of jumping back to the first item in the list.

B. Marker taxonomy: corrected events now render person marker (reviewer), not
   sparkle. Auto-resolved events (auto: true) render sparkle (agent) instead of
   the UserRoundCheck resolved marker.

C. correctionPulse added to InvoiceRuntime; correctDetail computes it atomically
   (detailFields, lineNums, autoResolvedIds). DetailsCombinedTab settle-pulses
   corrected scalar fields and qty cells. FindingsMap rows settle-pulse when their
   ID is in autoResolvedIds. Edit-mode save defers correctDetail until 200ms after
   exit so the pulse fires on the visible panel, not the hidden overlay.

D. DetailsEditForm section labels: remove uppercase/tracking-wide Tailwind classes
   so "Invoice details", "Parties", "Line items" render in sentence-case weight.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds resetAllRuntimes() to the runtime store — wipes in-memory map,
cursor map, and sessionStorage in one call. Surfaces as a small RotateCcw
icon button next to "My Work" in the LeftNav header, with a tooltip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hovering or focusing a data-mutating fix action (suggest_correction, verify)
shows a 1.5px dashed warning-toned outline on the evidence value in the
exception card and on the matching field/line in the Details panel. Non-
mutating actions (route, wait) produce no aim. verify aims at evidence only
(empty correction = no panel fields targeted). Aim clears instantly on
mouseleave/blur.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…do toast

A. Unifies the evidence-value affordance: replaces the Highlighter with a Pencil
   on every warn-toned FindingCell, removes the sourceAnchors guard so the pen
   always shows, and expands EXCEPTION_EDIT_FIELD with outside-po-period,
   new-vendor, and billing-account mappings. onRequestEdit now accepts an
   optional field key so it opens edit mode even when no direct mapping exists.

B. Ghost preview on aim: while a mutating fix action is hovered/focused, the
   warn evidence cell shows "current → new" inside the dashed ring and the
   corresponding panel metadata field echoes the same inline preview. Line-item
   cells stay ring-only. aimGhostValue is computed from the first scalar entry
   in rt.aimCorrection and threaded through ExceptionGroup → Stage →
   LiveExceptionContent → FindingView → FindingCell.

C. Undo via toast: after any correction commit a Sonner toast shows "Corrected
   {Field}" or "{N} fields corrected" with an 8s Undo action. Undo calls
   revertDetail which restores detailCorrections to the pre-correction snapshot,
   un-resolves the auto-cleared exceptions, and appends a combined
   "Correction reverted" person event + "Re-validated" sparkle event. Previous
   toast is dismissed before the new one appears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts, Bill to spacing fix

Replace FindingsMap + ExceptionIndex with IssueDots pagination dots and carousel
prev/next arrow buttons on the "Needs your decision" header row. One dot per
non-waiting issue; pending = hollow circle, current = amber pill, resolved = muted
fill with settle pulse on auto-resolve. Arrow buttons are 26px, hairline border,
disabled-at-ends; keyboard ←/→ works when focus is in the decision header. Single
issue hides arrows and dots. Removes the exceptionListVariant prop entirely.

Also adds mt-0.5 to the Bill to address block so its spacing from the company name
matches the Vendor section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each edit-form input whose field participates in an open finding renders a
reference line beneath it: counterpart label, value, provenance, and a
"Use this value" action that fills the input and marks it sourced.

On save, sourced corrections get per-field events noting the origin
("applied vendor master value", "applied PO quantity") instead of the
plain old→new copy. Manually typed corrections keep the old→new format.

Annotations live-derive from the open exception set, so resolved findings
drop their reference rows and undo restores them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce seam

Adds an explicit `reference` seam to InvoiceException: each exception that
implicates a specific editable field now carries the system-of-record value
the reviewer should correct toward.

Seeded on: INV-GRN-001 price mismatch (PO amount), INV-66216 VAT mismatch
(vendor master), INV-55832 outside PO period (PO window end date), and
INV-30291/INV-30292 qty-over-invoiced exceptions (PO qty per line).

The edit form derives annotations from live open exceptions via
referenceFieldKeyToFormKey. Each annotated input shows the reference value
beneath it; Use this value fills the field. On save, corrections whose final
value matches the reference produce applied-source event copy rather than
the plain old→new format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds optional shortDescription to the line item type; renders below the
item title as text-xs muted-foreground, truncated to one line with a
tooltip on hover exposing the full string. Seeded on INV-GRN-001 and
INV-66216 for visibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
petervachon and others added 12 commits July 24, 2026 07:46
…n issues

Navigation domain is now open issues only. Dots, arrows, and the jump menu
all traverse open issues; Issue n of m counts position among open, not total.
Resolved issues settle-then-disappear from dots; the jump menu drops Done rows
entirely. Counter and chrome hide when only one open issue remains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…suggestions

Mark verified and any future attest-shaped suggestions now show the dashed
aim ring on the attested field without a ghost arrow or incoming value. The
ring means "this value is what the action concerns" — not "this will change."

Empty-string sentinels in suggestionAimCorrection let isAimed fire (value
!== undefined) while aimGhost returns "" (falsy), suppressing the → X arrow.
resolveActive also guards detailCorrections from verify type so committing an
attestation never calls correctDetail — no field is written, no CorrectedMark
appears on the panel value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All verify/attest suggestions now say "Keep X" or "Confirm Y" instead of the
generic "Mark verified"/"Mark reviewed". INV-66216 VAT card gets three distinct
actions (Correct, Keep invoice value, Route). Per-suggestion resolution overrides
let each fix path emit its own event copy. suggestionLabel gains a verify fallback
and VAT auto-derivation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix cards carry at most two buttons (direct fix + Send to owner) and a ✕
icon at the header-right for the keep-as-is attestation path. ✕ hover fires
the aim ring (empty sentinels); click commits through the store and shows an
undo toast. Route labels renamed to "Send to owner" with a tooltip disclosing
the full recipient. INV-55832 gains a verify suggestion for the ✕ slot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…is visible

When hovering a mutation fix and its incoming value is already shown in the
evidence pair's reference column (e.g. "On file: US-82-4470911"), suppress the
inline → ghost on the target cell and switch to pair-mode: solid warning ring on
the source (reference) cell, existing dashed ring on the warn (target) cell, and
a small ← arrow at the divider pointing source→target. Mouseleave clears all
three atomically. Ghost previews in the Details panel and edit form are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Center-context hover (fix action buttons, ✕) shows aim rings and ghosts only
in the evidence pair chips inside ExceptionTimeline. The Details panel no longer
reacts to rt.aimCorrection during hover — it remains completely still. Commit
pulses (correctionPulse nonce) are unchanged, as are edit-form aim chips which
use their own local context. Batch-line aim follows the same rule: panel cells
ring only after commit, never during hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tions

When a fix-action (agent-sourced) commits, the corrected panel value
briefly glows with an inset box-shadow built from --ai-glow-start /
--ai-glow-end tokens, then settles to clean text. Manually typed
corrections settle with neutral amber peak → transparent rest. Removes
the lingering amber box that was persisting on corrected values — amber
is reserved for flagged state, not fixed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three-beat sequence replaces the landing glow for agent-sourced fixes:
fix button shows Applying… spinner (disabled) while the Details panel
shows an AI-gradient ring + anchored pill; at ~500ms correctDetail
commits (value swaps, undo toast + events fire); ring transitions to
emerald success and pill reads Updated; at ~1.6s both fade to clean.

Manual/typed corrections retain the neutral amber settle unchanged.
Reduced motion skips the arc entirely (instant rest state).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split correctDetail into step1 (ACT) / step2 (REST) so the center timeline,
queue chip, and carousel stay frozen while the panel pill is visible. The
correction and one corrected event land at ACT; auto-resolve IDs, auto-clear
events, the re-validated event, and the carousel advance all fire at REST via
correctDetailStep2. Adds rt.arcState guard to resolveActive to prevent
concurrent arcs during the 1800ms sequence.

Range-aim containment: replaces the equality-only sourceCell check in
FindingView with c.value.includes(aimGhostValue), so date windows and numeric
ranges trigger pair-mode (ring on counterpart, arrow at divider, no ghost) when
the incoming value appears anywhere in the counterpart string. Covers
INV-55832 outside-po-period where Apr 30, 2026 is the end of Jan 1 - Apr 30,
2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…em corrections

Timeline typography: canonical ChildRow component (13/500 label, 12/400 desc+ts)
shared by AgentHistoryPeek and CollapsedClusterRow; SubEventRow delegates to it.
Leading chevrons on both peek systems; ml-4/py-2 fold spacing.

Arc phase machine: glow overlay opacity now driven by arcState.phase (not a
1700ms CSS keyframe with its own clock). CLAIM fades glow in over 450ms ease-out,
ACT+CONFIRM hold at 0.15, RELEASE fades out over 300ms ease-in. Timers aligned:
ACT→500ms, CONFIRM→750ms, RELEASE→1350ms, REST→1650ms.

Line-item corrections: price-mismatch predicate added to RESOLUTION_PREDICATES.
exc-price (GRN-001) and exc-30292-qoi-1/3 gain correction payloads (amount+qty).
INV-30291 corrections include corrected line amounts. DetailsCombinedTab and
TopBarNext derive correctedLinesTotal/correctedAmount from correctedLines so
panel total and top-bar Amount cascade when line amounts are corrected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire TopBarNext: doApprove routes through runtime.approveGated so the
store blocks approval even if the disabled prop is bypassed via devtools.
Adds doWait/doSendBack, threads openCount/waitingCount into HeaderDecision,
and inserts waitingDisp into the effectiveStatus chain before onHoldDisp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-step feedback flow on the fix card footer: positive path records
sentiment and closes; correction path collects chips + note, writes a
person event immediately and a sparkle event after a 2s regen interval,
then shows seeded no-change prose. revisionCount derived from correction
records; superseded prose appended to audit trail only. Primary-filled
split button on the PO action, footer legal line with info icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 11:51
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Apollo Coded App preview deployments are ready.

Project Status Preview Updated (PT)
apollo-design Ready Preview · Logs Jul 24, 2026, 05:44:05 AM
apollo-docs Ready Preview · Logs Jul 24, 2026, 05:44:05 AM
apollo-landing Ready Preview · Logs Jul 24, 2026, 05:44:05 AM
apollo-vertex Ready Preview · Logs Jul 24, 2026, 05:44:05 AM

@github-actions

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1950 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1720
ISC 89
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

};
return {
...prev,
[id]: {
const cleared = openExceptions
.filter((exc) => {
const predicate = RESOLUTION_PREDICATES[exc.type];
return predicate !== undefined && !predicate(exc, corrections, base);
message.external_id &&
inv.messages.some((m) => m.external_id === message.external_id)
) {
state.invoices[invoiceId] = inv;
return false;
}
inv.messages.push(message);
state.invoices[invoiceId] = inv;

function resetInvoice(invoiceId) {
const state = readState();
state.invoices[invoiceId] = blankInvoice();
const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {
let body = "{}";
const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {
let body = "{}";
} = require("./escalation-card");
const store = require("./store");

const log = (...args) => console.log("[SLACK]", ...args);
const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {
let body = "{}";
const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {
let body = "{}";

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Draft v3 of the Apollo Vertex invoice exception review prototype UI, plus a demo-grade Slack roundtrip listener and supporting runtime/store wiring. This lives under apps/apollo-vertex/templates/invoice-review/ and introduces new UI primitives (header split dispositions, suggested-fix feedback flow, supplier email modal) backed by an in-memory + sessionStorage runtime store.

Changes:

  • Adds v3 invoice-review prototype surfaces (Suggested Fix feedback loop, supplier email modal, header disposition split button, runtime store updates).
  • Introduces a standalone Slack Socket Mode listener (outside pnpm workspace) with JSON-file state sharing and Next.js proxy API routes for the demo.
  • Adds shell extensibility (profile menu “extras” slot), new theme tokens, and a lint-time “type drift” guard script.

Reviewed changes

Copilot reviewed 32 out of 35 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
package.json Adds root-level demo scripts to run Apollo Vertex + Slack listener together
apps/apollo-vertex/templates/invoice-review/README.md Documents prototype status and lint-exclusion/migration guidance
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx New supplier email draft/confirm modal with send animation
apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx New suggested-fix card with split-button PO linking + feedback regen flow
apps/apollo-vertex/templates/invoice-review/next/ReasonDialog.tsx Shared “reason + note” dialog for park-family actions
apps/apollo-vertex/templates/invoice-review/next/invoice-runtime.tsx Runtime store for invoice loop state, events, gating, and undo behavior
apps/apollo-vertex/templates/invoice-review/next/invoice-review-data.ts Canonical invoice-review data model, fixtures, and seam stubs
apps/apollo-vertex/templates/invoice-review/next/HeaderDecision.tsx Header disposition split button and associated dialogs
apps/apollo-vertex/templates/invoice-review/invoice-version.tsx Adds a dev-only layout version switch (v1/v2/v3)
apps/apollo-vertex/templates/invoice-review/invoice-review.module.css Adds AI correction glow keyframes/util class
apps/apollo-vertex/slack/store.js File-backed shared demo state store with atomic writes
apps/apollo-vertex/slack/server.js Slack Socket Mode listener (actions, thread ingestion, local HTTP trigger)
apps/apollo-vertex/slack/reset-demo.js Resets Slack demo state and deletes bot messages
apps/apollo-vertex/slack/README.md Setup/run instructions and Slack manifest for the listener
apps/apollo-vertex/slack/package.json Isolated npm package for the Slack listener dependencies
apps/apollo-vertex/slack/escalation-card.js Block Kit escalation card builder for the demo invoice
apps/apollo-vertex/scripts/check-type-drift.mjs Lint-time guard to prevent off-scale text-[Npx] drift in timeline
apps/apollo-vertex/registry/shell/shell-user-profile-menu-items.tsx Renders injected profile menu extras
apps/apollo-vertex/registry/shell/shell-profile-extras.tsx New context/provider for profile menu extra items slot
apps/apollo-vertex/registry/button/button.tsx Tweaks base button classes (adds pointer cursor)
apps/apollo-vertex/registry.json Adds “insight-*” color tokens
apps/apollo-vertex/package.json Adds demo scripts + runs type-drift guard in lint
apps/apollo-vertex/locales/en.json Adds new i18n keys (“invoices”, “my_work”)
apps/apollo-vertex/lib/i18n.ts Prevents re-initializing i18n and syncs document lang on re-entry
apps/apollo-vertex/app/invoice-review/page.tsx Adds an invoice-review route wrapper rendering the template full-screen
apps/apollo-vertex/app/api/demo-trigger/route.ts Proxies escalation trigger to the local Slack listener
apps/apollo-vertex/app/api/demo-state/route.ts Serves demo Slack state JSON to the UI
apps/apollo-vertex/app/api/demo-reply/route.ts Proxies Comms replies to the local Slack listener
apps/apollo-vertex/app/_meta.ts Hides invoice-review route from nav/meta
apps/apollo-vertex/.oxlintrc.json Excludes demo/prototype directories from oxlint
apps/apollo-vertex/.gitignore Ignores demo runtime JSON and isolated Slack deps/lockfile
apps/apollo-vertex/.env.example Adds Slack demo environment variable template
Comments suppressed due to low confidence (3)

apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx:109

  • Clear the pending regen timeout on unmount to avoid state updates after the component is removed.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:63
  • Add refs + cleanup for the send animation timers to prevent leaking timers and updating state after unmount.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:85
  • Use the timer refs so repeated clicks don’t stack timers and so the cleanup effect can cancel the pending work.

Comment thread package.json
Comment on lines +23 to +24
"demo": "apps/apollo-vertex/slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm --filter apollo-vertex dev\" \"cd apps/apollo-vertex/slack && npm start\"",
"demo:reset": "cd apps/apollo-vertex/slack && npm run reset-demo",
Comment on lines +9 to +10
"demo": "slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm dev\" \"cd slack && npm start\"",
"demo:reset": "cd slack && npm run reset-demo",
Comment on lines 7 to 9
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
Pencil,
X,
} from "lucide-react";
import { useRef, useState } from "react";
"use client";

import { Check, Loader2, Send } from "lucide-react";
import { useId, useRef, useState } from "react";
Comment on lines +194 to +198
* Approve only if openCount + waitingCount = 0 at commit time. Returns true and
* sets the approved disposition; returns false without any state change if the
* guard fails. This is the authoritative gate — the button's disabled prop is
* only a visual hint. The caller should skip the seam stub if false is returned.
*/

const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {

const LISTENER_PORT = process.env.LISTENER_PORT || "3010";

export async function POST(request: NextRequest) {
// review UI polls this every couple seconds to reflect Slack-driven actions.
export const dynamic = "force-dynamic";

export async function GET() {
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package bundle size on this PR (no JS/TS source changes detected under packages/* or web-packages/*).

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core 9.0% 43.84 MB 57.45 MB ±0
@uipath/apollo-react 38.7% 7.45 MB 28.56 MB ±0
@uipath/apollo-wind 41.5% 401.6 KB 2.60 MB +21 B
@uipath/ap-chat 85.8% 43.44 MB 55.94 MB ±0

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 35 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (5)

apps/apollo-vertex/registry/button/button.tsx:8

  • Adding cursor-pointer at the base Button level also applies to disabled buttons, which can make a disabled action still look clickable. Prefer overriding the cursor for the disabled state.
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",

package.json:24

  • The root demo scripts hardcode a path into apps/apollo-vertex/slack/node_modules/.bin, which breaks unless Slack deps have already been installed and is less portable. Since apollo-vertex already defines demo/demo:reset, the root scripts can delegate to those.
    apps/apollo-vertex/slack/server.js:222
  • lastMessage is never read, so this assignment is dead code (and can be removed along with the variable declaration).
  lastMessage = { channel: res.channel, ts: res.ts };

apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:65

  • handleSendClick uses nested setTimeout without tracking ids, so the callbacks can fire after the modal has closed/unmounted (calling setSendPhase / onSend). Track the timers in refs and clear them before scheduling new ones.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:92
  • When the dialog closes while a send animation is in flight, scheduled timeouts can still fire. Clear sendTimerRef/sentTimerRef when onOpenChange receives false so closing always cancels the pending callbacks.

Comment on lines +745 to +760
approveGated: (id, openCount, waitingCount, event) => {
if (openCount + waitingCount > 0) return false;
setMap((prev) => {
const cur = prev[id] ?? EMPTY;
const time = new Date().toISOString();
return {
...prev,
[id]: {
...cur,
disposition: { type: "approved", time },
events: [...cur.events, ...withKeys(id, cur.events, [event])],
},
};
});
return true;
},
Comment on lines +173 to +176
// Thread replies only — top-level channel posts are ignored.
if (!message.thread_ts) return ignore("not a thread reply");

const invoiceId = store.invoiceForThread(message.thread_ts);
Comment on lines +40 to +42
// Remember the most recently posted escalation card so later steps can update
// it (chat.update) after an action is taken.
let lastMessage = null;
Comment on lines +10 to +15
"dependencies": {
"@slack/bolt": "^4.4.0"
},
"devDependencies": {
"concurrently": "^9.1.0"
}
Comment on lines +29 to +35
return NextResponse.json(
{
ok: false,
error: `Slack listener not reachable on :${LISTENER_PORT}. Start it with: cd slack && npm start`,
},
{ status: 502 },
);
Comment on lines +26 to +32
return NextResponse.json(
{
ok: false,
error: `Slack listener not reachable on :${LISTENER_PORT}. Start it with: cd slack && npm start`,
},
{ status: 502 },
);
…r sweep

Rename card header to "Suggested next step" with text-base tracking-tight
font-semibold. Apply ai-gradient-text to "Best match" badge. Complete
vendor→supplier rename across all remaining user-facing prose strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 12:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 35 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (7)

package.json:24

  • The root-level demo scripts depend on apps/apollo-vertex/slack/node_modules/.bin/concurrently, which will fail unless the Slack listener has been npm installed and also hardcodes a non-portable node_modules path. Prefer delegating to the apollo-vertex package scripts so the entry point is stable.
    apps/apollo-vertex/package.json:10
  • This demo script hardcodes slack/node_modules/.bin/concurrently, which breaks on a fresh clone until someone runs npm install in slack/ and is also brittle across OS/shells. Consider adding concurrently to this package (pnpm workspace) and using pnpm exec concurrently, or move the Slack listener into the workspace so pnpm -r can manage it.
    "dev": "pnpm generate:theme && next --turbopack",
    "demo": "slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm dev\" \"cd slack && npm start\"",
    "demo:reset": "cd slack && npm run reset-demo",

apps/apollo-vertex/app/api/demo-trigger/route.ts:14

  • This demo-only API route proxies requests to http://localhost:${LISTENER_PORT}. If apollo-vertex is ever built/deployed outside local dev, leaving this endpoint enabled is an avoidable SSRF/operational risk (it will attempt to talk to a localhost service on the server). Add a production guard (or require an explicit opt-in env flag) and return 404/disabled in prod.
export async function POST(request: NextRequest) {
  let body = "{}";
  try {
    body = JSON.stringify(await request.json());
  } catch {

apps/apollo-vertex/app/api/demo-reply/route.ts:14

  • This demo-only API route proxies requests to http://localhost:${LISTENER_PORT}. If apollo-vertex is ever built/deployed outside local dev, leaving this endpoint enabled is an avoidable SSRF/operational risk. Add a production guard (or require an explicit opt-in env flag) and return 404/disabled in prod.
export async function POST(request: NextRequest) {
  let body = "{}";
  try {
    body = JSON.stringify(await request.json());
  } catch {

apps/apollo-vertex/app/api/demo-state/route.ts:13

  • This demo-only endpoint reads a local file and returns its contents. If this app is deployed, the route will still be callable and may leak environment-specific state or behave unexpectedly. Consider guarding it behind an explicit dev-only flag (or returning 404 in production).
export async function GET() {
  try {
    const file = path.join(process.cwd(), "data", "demo-state.json");
    const raw = await readFile(file, "utf8");
    return NextResponse.json(JSON.parse(raw));

apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:84

  • handleSendClick schedules nested setTimeout calls without any cleanup. If the modal unmounts (or open flips) before the timers fire, this can call setSendPhase on an unmounted component and also invoke onSend after teardown. Track timeout IDs in refs and clear them in an effect cleanup (and/or guard with an isMounted ref).
    apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx:213
  • The regen flow schedules a 2s timer via regenTimerRef, but there is no unmount cleanup. Navigating away while the timer is pending can trigger setRegenLoading(false) on an unmounted component and fire onFeedbackRegen unexpectedly. Add a useEffect cleanup to clear regenTimerRef.current on unmount.

//
// Re-entrancy rule: every action that changes state appends its event(s) in the
// SAME mutation (never from a view effect), so the append-only log can't
// double-fire or half-persist. Keys are `evt-${id}-${length + indexInBatch}` so
log("conversations.history failed:", e.data?.error || e.message);
}

store.resetAll(); // clears invoices + postedMessages back to initial
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:apollo-vertex size:XXL 1,000+ changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants