Skip to content

Invoice Process: Execption UI v3#908

Closed
petervachon wants to merge 44 commits into
mainfrom
invoice-exception-v3
Closed

Invoice Process: Execption UI v3#908
petervachon wants to merge 44 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 21, 2026, 03:39:47 AM
apollo-docs 🟢 Ready Preview, Logs Jul 21, 2026, 03:39:47 AM
apollo-landing 🟢 Ready Preview, Logs Jul 21, 2026, 03:39:47 AM
apollo-vertex 🟢 Ready Preview, Logs Jul 21, 2026, 03:39:47 AM

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

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.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 +36 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.

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"
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"];
Copilot AI review requested due to automatic review settings July 19, 2026 20:34
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from 05e4b5e to 5535b9f Compare July 19, 2026 20:34

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 6 comments.

Comments suppressed due to low confidence (1)

package.json:24

  • The root demo script hardcodes a nested node_modules/.bin/concurrently path, which is brittle (breaks if deps aren’t installed yet and is not portable across environments). Since apollo-vertex already defines demo/demo:reset, prefer delegating to that package’s scripts so there’s a single source of truth.

Comment on lines +127 to +132
<Button variant="ghost" size="sm" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant={commitVariant} size="sm" onClick={commit}>
{commitLabel}
</Button>
Comment on lines +219 to +228
<Button
variant="secondary"
size="sm"
disabled={disabled}
onClick={() => setRouteDialogOpen(true)}
onMouseEnter={handleAim}
onFocus={handleAim}
onMouseLeave={handleClearAim}
onBlur={handleClearAim}
>
Comment on lines +250 to +259
<Button
variant="secondary"
size="sm"
disabled={disabled}
onClick={() => onResolve(suggestion)}
onMouseEnter={handleAim}
onFocus={handleAim}
onMouseLeave={handleClearAim}
onBlur={handleClearAim}
>
"use client";

import { Check, Loader2, Send } from "lucide-react";
import { useId, useRef, useState } from "react";
Comment on lines +61 to +85
const bodyRef = useRef<HTMLTextAreaElement>(null);
const markGradientId = useId();
const busy = sendPhase !== "idle";
// The body is focused programmatically on open; text fields still match
// :focus-visible on programmatic focus, so gate the ring on a real interaction
// (keydown / pointer) to keep the open state ring-free with just its border.
const [ringArmed, setRingArmed] = useState(false);
const [bodyEdited, setBodyEdited] = useState(false);

function buildDraft(): SupplierEmailDraft {
return { to, cc: cc.trim() || undefined, subject, body };
}

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 +365 to +369
server.listen(LISTENER_PORT, () =>
log(
`HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
),
);
Copilot AI review requested due to automatic review settings July 20, 2026 11:14
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from 5535b9f to df828b4 Compare July 20, 2026 11:14
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from df828b4 to 8921124 Compare July 20, 2026 11:17

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.

Comments suppressed due to low confidence (2)

package.json:24

  • The root-level demo scripts hardcode the Slack listener’s local node_modules path (apps/apollo-vertex/slack/node_modules/.bin/concurrently). That makes pnpm demo brittle and duplicates the same logic already added to apps/apollo-vertex. Prefer delegating to the workspace script so the root stays stable even if the Slack folder changes.
    apps/apollo-vertex/slack/server.js:369
  • The local HTTP trigger server binds without an explicit host, which typically listens on all interfaces. Combined with permissive CORS (Access-Control-Allow-Origin: *), this makes it easier for other machines on the network (or a malicious webpage) to hit the trigger endpoints during a demo. Bind explicitly to 127.0.0.1 to keep it local-only.
  server.listen(LISTENER_PORT, () =>
    log(
      `HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
    ),
  );

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",
{
Comment on lines +182 to +193
const [cursorMap, setCursorMap] = useState<Record<string, string>>({});

const value = useMemo<RuntimeStore>(
() => ({
getRuntime: (id) => map[id] ?? EMPTY,
getCursor: (id) => cursorMap[id],
setCursor: (id, exceptionId) =>
setCursorMap((prev) => {
const next = exceptionId ?? "";
if (prev[id] === next) return prev;
return { ...prev, [id]: next };
}),
() =>
resolve({
id: `${invoiceId}-${exceptionId}-msg`,
sentTime: "just now",
Comment on lines +10 to +11
export async function POST(request: NextRequest) {
let body = "{}";
Comment on lines +10 to +11
export async function POST(request: NextRequest) {
let body = "{}";
Copilot AI review requested due to automatic review settings July 20, 2026 11:19

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 3 comments.

Comments suppressed due to low confidence (2)

package.json:24

  • The root-level demo scripts hardcode the Slack listener’s local node_modules bin path and duplicate the same orchestration that now exists in apps/apollo-vertex/package.json. This is brittle (breaks if slack deps aren’t installed yet, and is less portable across shells/OSes) and creates two sources of truth for the demo command.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:84
  • handleSendClick schedules nested setTimeout callbacks without any cleanup. If the modal unmounts (navigation, parent state reset, React strict-mode re-mounting in dev), these timeouts can fire after unmount and call setSendPhase/onSend unexpectedly. Consider storing timeout IDs in a ref and clearing them in a cleanup effect (and/or before scheduling a new send).

Comment on lines +17 to +28
try {
const res = await fetch(
`http://localhost:${LISTENER_PORT}/trigger-escalation`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body,
},
);
const data = await res.json();
return NextResponse.json(data, { status: res.ok ? 200 : 502 });
} catch {
Comment on lines +17 to +25
try {
const res = await fetch(`http://localhost:${LISTENER_PORT}/reply`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
const data = await res.json();
return NextResponse.json(data, { status: res.ok ? 200 : res.status });
} catch {
Comment on lines +51 to +61
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
const migrated =
saved === "current" ? "v1" : saved === "next" ? "v2" : saved;
if (isVersion(migrated)) setVersionState(migrated);
}, []);

const setVersion = (v: InvoiceVersion) => {
setVersionState(v);
localStorage.setItem(STORAGE_KEY, v);
};
Copilot AI review requested due to automatic review settings July 20, 2026 11:35

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 6 comments.

Comments suppressed due to low confidence (2)

package.json:24

  • The demo/demo:reset scripts hardcode apps/apollo-vertex/slack/node_modules/.bin/concurrently, which will fail unless the Slack listener dependencies have been installed and the path layout matches exactly. Prefer invoking a pinned concurrently via pnpm so the script works from a clean checkout without relying on another subproject’s node_modules.
    apps/apollo-vertex/package.json:10
  • This demo script depends on slack/node_modules/.bin/concurrently, which is brittle and breaks if apps/apollo-vertex/slack hasn’t been npm install’d yet. Using a pinned pnpm dlx (or adding concurrently to this package’s devDependencies and using pnpm exec concurrently) makes the command runnable from a clean checkout.
    "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 +1 to +2
"use client";
import { InvoiceReviewTemplate } from "@/templates/invoice-review/InvoiceReviewTemplate";
Comment on lines +142 to +146
<input
value={to}
onChange={(e) => setTo(e.target.value)}
className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
/>
Comment on lines +162 to +167
<input
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="Add CC…"
className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring"
/>
Comment on lines +174 to +178
<input
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
/>
Comment on lines +202 to +206
<textarea
ref={bodyRef}
value={body}
onChange={(e) => {
setBody(e.target.value);
Comment on lines +107 to +113
<Button
key={c}
type="button"
variant={reason === c ? "default" : "outline"}
size="sm"
onClick={() => setReason(c)}
>
petervachon and others added 24 commits July 24, 2026 07:45
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>
…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:47
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from 78e3e0d to e254a61 Compare July 24, 2026 11:47
@petervachon

Copy link
Copy Markdown
Collaborator Author

Closing in favour of a fresh draft PR.

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 1 comment.

Comments suppressed due to low confidence (9)

package.json:24

  • The root demo scripts hardcode the Slack listener's local node_modules/.bin path, which will fail unless the Slack subfolder has been npm-installed and also duplicates the package-level scripts. Prefer delegating to the apollo-vertex package scripts so the root stays stable if paths change.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:4
  • This component starts send-animation timers; adding a cleanup effect requires importing useEffect from React to avoid setState calls after unmount.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:67
  • handleSendClick uses nested setTimeout calls without cleanup. If the modal unmounts (e.g., parent conditionally renders it), these callbacks can fire after unmount and attempt state updates. Store the timer IDs in refs and clear them in a cleanup effect.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:146
  • The "To" input is visually labeled but has no programmatic label, so screen readers will announce it as an unlabeled textbox. Add an aria-label (or associate a ) and set an appropriate input type.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:167
  • The "CC" input is visually labeled but lacks a programmatic label, which harms screen reader usability. Add an aria-label (or associate a ) and set an appropriate input type.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:178
  • The "Subject" input is visually labeled but has no programmatic label. Add an aria-label (or associate a ) so assistive tech announces it correctly.
    apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx:109
  • regenTimerRef is cleared before starting a new timer, but it's not cleared on unmount. Add a cleanup effect so the pending timeout can't fire after the card is removed.
    apps/apollo-vertex/slack/server.js:304
  • This local HTTP listener sets permissive CORS headers, but the prototype reaches it server-to-server via Next.js proxy routes. Removing these headers reduces the chance of unintended cross-origin browser access when the listener is running.
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");

apps/apollo-vertex/slack/server.js:369

  • server.listen() currently binds on all interfaces by default, which can expose the demo endpoints (/trigger-escalation and /reply) to the local network and allow posting into Slack if someone can reach the port. Bind explicitly to 127.0.0.1 for local-only access.
  server.listen(LISTENER_PORT, () =>
    log(
      `HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
    ),
  );

Pencil,
X,
} from "lucide-react";
import { useRef, useState } from "react";
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