Skip to content

Invoice Process: Execption UI v3#908

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

Invoice Process: Execption UI v3#908
petervachon wants to merge 40 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 20, 2026, 05:43:42 AM
apollo-docs 🟢 Ready Preview, Logs Jul 20, 2026, 05:43:42 AM
apollo-landing 🟢 Ready Preview, Logs Jul 20, 2026, 05:43:42 AM
apollo-vertex 🟢 Ready Preview, Logs Jul 20, 2026, 05:43:42 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.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.3% 7.38 MB 28.15 MB ±0
@uipath/apollo-wind 41.0% 397.4 KB 2.58 MB −29 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"
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)`,
),
);
petervachon and others added 11 commits July 20, 2026 07:13
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>
petervachon and others added 7 commits July 20, 2026 07:13
…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>
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 and others added 3 commits July 20, 2026 07:17
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>
@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)}
>
…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>
Copilot AI review requested due to automatic review settings July 20, 2026 11:55
@petervachon
petervachon force-pushed the invoice-exception-v3 branch from 3d7bcf1 to 3aed9d7 Compare July 20, 2026 11:55

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

Comments suppressed due to low confidence (7)

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

  • cursor-pointer is applied unconditionally, so disabled buttons (or aria-disabled buttons) can still show a pointer cursor. Prefer enabled:cursor-pointer so only interactive buttons show the pointer affordance.
  "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",

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

  • handleSendClick schedules nested setTimeout calls but doesn't clear them on close/unmount. If the dialog closes quickly, this can set state on an unmounted component and can also call onSend after discard. Track timeout IDs in refs and clearTimeout them in an effect cleanup and when discarding.
    apps/apollo-vertex/slack/server.js:369
  • The local HTTP trigger server listens on all interfaces by default and also sets Access-Control-Allow-Origin: *. Even for a demo, this makes the unauthenticated endpoints (/trigger-escalation, /reply) reachable from other machines on the network. Bind explicitly to localhost to keep it local-only.
  server.listen(LISTENER_PORT, () =>
    log(
      `HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
    ),
  );

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

  • The "To" input has no accessible name. Add an aria-label (or a real <label> association) so screen readers can identify the field.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:167
  • The "CC" input has no accessible name. Add an aria-label (or a real <label> association) so screen readers can identify the field.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:178
  • The "Subject" input has no accessible name. Add an aria-label (or a real <label> association) so screen readers can identify the field.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:205
  • The email body textarea has no accessible name. Add an aria-label (or connect it to visible text via aria-labelledby) so screen readers can announce it properly.

Comment on lines +33 to +36
if (i18n.isInitialized) {
document.documentElement.lang = i18n.language;
return;
}
Comment on lines +112 to +114
const invoiceId = payload.invoice_id;
const act = payload.action; // approve | hold | reject

/** optional supporting line; also the slot for declaring side effects */
description?: string;
/** reason options; the first (or defaultChip) is preselected */
chips: readonly string[];
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>
Copilot AI review requested due to automatic review settings July 20, 2026 12:38

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 (8)

package.json:24

  • The root-level demo scripts hardcode a path into the Slack demo's local node_modules (apps/apollo-vertex/slack/node_modules/.bin/concurrently). That makes pnpm demo fail unless the Slack listener has already been installed, and it’s also brittle/cross-platform unfriendly. Prefer delegating to the app-level scripts instead, so there’s a single source of truth.
    apps/apollo-vertex/package.json:10
  • This demo script shells out to slack/node_modules/.bin/concurrently, which is brittle (platform-specific) and couples the app script to the Slack listener’s install layout. Using npm --prefix slack exec -- concurrently keeps the dependency local to the Slack listener while avoiding hardcoded .bin paths.
    "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/templates/invoice-review/next/ReasonDialog.tsx:132

  • The dialog’s footer buttons don’t specify type="button", and the commit action can fire with an empty reason when chips is empty (since initialChip falls back to ""). Making the button types explicit and disabling commit when no reason is selected avoids accidental submits and invalid commits.
        <DialogFooter>
          <Button variant="ghost" size="sm" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button variant={commitVariant} size="sm" onClick={commit}>
            {commitLabel}
          </Button>

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

  • The "To" field input is missing an accessible name (no and no aria-label). This makes the form harder to use with screen readers. Add an aria-label (or wire up a real ) and consider using type="email" for basic validation/IME hints.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:167
  • The "CC" field input is missing an accessible name (no and no aria-label). Add an aria-label (or a real label) so assistive tech can identify the field.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:178
  • The "Subject" field input is missing an accessible name (no and no aria-label). Add an aria-label (or a real label) so assistive tech can identify the field.
    apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:205
  • The email body textarea is missing an accessible name (no and no aria-label). Add an aria-label (or connect a visible label) so screen readers announce what the control is for.
    apps/apollo-vertex/templates/invoice-review/next/invoice-runtime.tsx:207
  • setCursor stores an empty string when clearing the cursor (exceptionId undefined). That means getCursor() can return "" (which doesn’t match its string | undefined contract) and also leaves a permanent key in cursorMap even when no cursor exists. Prefer deleting the key (or storing undefined) when clearing.

Comment on lines +73 to +79
<Button
aria-disabled
onClick={(e) => e.preventDefault()}
className={cn("opacity-50", "cursor-not-allowed")}
>
Approve
</Button>
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