Skip to content

Invoice Process: Execption UI v3#908

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

Invoice Process: Execption UI v3#908
petervachon wants to merge 33 commits into
mainfrom
invoice-exception-v3

Conversation

@petervachon

Copy link
Copy Markdown
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings July 13, 2026 16:29
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Review Updated (PT)
apollo-design 🟢 Ready Preview, Logs Jul 17, 2026, 12:54:20 PM
apollo-docs 🟢 Ready Preview, Logs Jul 17, 2026, 12:54:20 PM
apollo-landing 🟢 Ready Preview, Logs Jul 17, 2026, 12:54:20 PM
apollo-vertex 🟢 Ready Preview, Logs Jul 17, 2026, 12:54:20 PM

@github-actions

github-actions Bot commented Jul 13, 2026

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.2.4 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

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 = "{}";
@github-actions

github-actions Bot commented Jul 13, 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.82 MB 57.31 MB ±0
@uipath/apollo-react 37.2% 7.38 MB 28.15 MB ±0
@uipath/apollo-wind 40.3% 395.1 KB 2.57 MB +10 B
@uipath/ap-chat 85.8% 43.43 MB 55.92 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.

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

Adds an “Invoice Review” prototype flow to Apollo Vertex (v3 exception/timeline UI) and a bidirectional Slack demo integration, including local demo API routes and supporting runtime/state utilities.

Changes:

  • Introduces a new invoice-review prototype template with a runtime store, timeline UI, decision dialogs, and fixture data.
  • Adds a standalone Slack Socket Mode listener (isolated npm package) plus reset tooling, and Next.js API proxies to bridge the prototype ↔ listener.
  • Extends the Vertex shell profile menu with an “extras” slot and adds supporting demo/dev scripts, tokens, and locale strings.

Reviewed changes

Copilot reviewed 32 out of 34 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package.json Adds root-level demo scripts to run the Vertex app + 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 Supplier email draft/send modal used in the routing flow.
apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx Renders AI suggested fix card and route actions (incl. reason dialog).
apps/apollo-vertex/templates/invoice-review/next/ReasonDialog.tsx Shared confirm dialog with reason chips + optional note.
apps/apollo-vertex/templates/invoice-review/next/invoice-runtime.tsx Adds per-invoice runtime store (events, waiting, disposition, highlights).
apps/apollo-vertex/templates/invoice-review/next/invoice-review-data.ts Adds canonical types, fixtures, and seam/stub functions for the prototype.
apps/apollo-vertex/templates/invoice-review/next/HeaderDecision.tsx Header disposition control (approve + hold/reject overflow using ReasonDialog).
apps/apollo-vertex/templates/invoice-review/next/ExceptionTimeline.tsx Large timeline UI implementation with resolve choreography + waiting/terminal states.
apps/apollo-vertex/templates/invoice-review/invoice-version.tsx Dev-only layout version switch (v1/v2/v3) persisted in localStorage.
apps/apollo-vertex/slack/store.js JSON-file store for demo invoice state written by Slack listener.
apps/apollo-vertex/slack/server.js Slack Bolt Socket Mode listener + local HTTP trigger/proxy endpoints.
apps/apollo-vertex/slack/reset-demo.js Script to delete bot messages and reset demo store between rehearsals.
apps/apollo-vertex/slack/README.md Setup/run docs for the Slack listener and demo flow.
apps/apollo-vertex/slack/package.json Defines isolated npm package for Slack listener dependencies/scripts.
apps/apollo-vertex/slack/escalation-card.js Block Kit escalation card builder posted into Slack.
apps/apollo-vertex/scripts/check-type-drift.mjs Adds a font-size “type drift” guard for the new timeline UI.
apps/apollo-vertex/registry/shell/shell-user-profile-menu-items.tsx Renders injected profile-menu extras via new hook/slot.
apps/apollo-vertex/registry/shell/shell-profile-extras.tsx Adds provider/hook to inject extra items into the shell profile menu.
apps/apollo-vertex/registry/button/button.tsx Updates button base classes (adds cursor-pointer).
apps/apollo-vertex/registry.json Adds “insight-*” color tokens used by the new UI.
apps/apollo-vertex/package.json Adds app-level demo scripts and extends lint with the type-drift check.
apps/apollo-vertex/locales/en.json Adds new strings (“invoices”, “my_work”).
apps/apollo-vertex/lib/i18n.ts Avoids re-initializing i18n; updates <html lang> when already initialized.
apps/apollo-vertex/app/invoice-review/page.tsx Adds a route that renders the invoice review template full-screen.
apps/apollo-vertex/app/api/demo-trigger/route.ts Proxies “trigger escalation” requests to the local Slack listener.
apps/apollo-vertex/app/api/demo-state/route.ts Serves the Slack listener’s shared demo-state JSON to the prototype.
apps/apollo-vertex/app/api/demo-reply/route.ts Proxies comms replies to Slack listener to post into Slack thread.
apps/apollo-vertex/app/_meta.ts Hides the invoice-review route from navigation/meta.
apps/apollo-vertex/.oxlintrc.json Excludes demo/prototype directories from oxlint.
apps/apollo-vertex/.gitignore Ignores demo runtime state + isolated Slack listener artifacts.
apps/apollo-vertex/.env.example Adds a template for Slack demo environment variables.

Comment on lines +302 to +304
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
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 +11 to +14
"@slack/bolt": "^4.4.0"
},
"devDependencies": {
"concurrently": "^9.1.0"
petervachon and others added 20 commits July 17, 2026 14:33
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>
petervachon and others added 13 commits July 17, 2026 14:33
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>
Replace annotation line with a compact chip (<value> · <source>) that acts
as the apply action directly. Dry-run runPredicates in handleSave to detect
still-failing exceptions and emit targeted re-check copy ("VAT number still
differs from vendor master") instead of the generic "nothing new". Fix
FIELD_LABELS casing so event labels read "Corrected VAT number" and
"Corrected document date" correctly in-sentence.

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

- Correction toast uses inverted (dark bg / light text) style via classNames
- "High value" subline replaced with Badge secondary/warning for a11y
- IssueDots reverted to uniform 7px bg-foreground dots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>
Copilot AI review requested due to automatic review settings July 17, 2026 19:48
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from 7ced88d to 05e4b5e Compare July 17, 2026 19:48
};
return {
...prev,
[id]: {
const cleared = openExceptions
.filter((exc) => {
const predicate = RESOLUTION_PREDICATES[exc.type];
return predicate !== undefined && !predicate(exc, corrections, base);

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 31 out of 34 changed files in this pull request and generated 5 comments.

Comment on lines +74 to +85
function handleSendClick() {
// Reduced motion: commit immediately, no sending -> sent beats.
if (reducedMotion) {
onSend(buildDraft());
return;
}
setSendPhase("sending");
setTimeout(() => {
setSendPhase("sent");
setTimeout(() => onSend(buildDraft()), 500);
}, 600);
}
Comment on lines +188 to +193
<button
type="button"
className="cursor-pointer hover:opacity-90"
>
{action}
</button>
Comment on lines +302 to +304
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
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",
// it (chat.update) after an action is taken.
let lastMessage = null;

const required = ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"];
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