Invoice Process: Execption UI v3#908
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency License Review
License distribution
Excluded packages
|
| 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 = "{}"; |
📊 Coverage + size by packagePer-package bundle size on this PR (no JS/TS source changes detected under
"Coverage" is each package's own |
There was a problem hiding this comment.
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. |
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |
| "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", |
| "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", |
| "@slack/bolt": "^4.4.0" | ||
| }, | ||
| "devDependencies": { | ||
| "concurrently": "^9.1.0" |
7ced88d to
05e4b5e
Compare
| 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); | ||
| } |
| <button | ||
| type="button" | ||
| className="cursor-pointer hover:opacity-90" | ||
| > | ||
| {action} | ||
| </button> |
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |
| "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"]; |
05e4b5e to
5535b9f
Compare
There was a problem hiding this comment.
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
demoscript hardcodes a nestednode_modules/.bin/concurrentlypath, which is brittle (breaks if deps aren’t installed yet and is not portable across environments). Sinceapollo-vertexalready definesdemo/demo:reset, prefer delegating to that package’s scripts so there’s a single source of truth.
| <Button variant="ghost" size="sm" onClick={() => setOpen(false)}> | ||
| Cancel | ||
| </Button> | ||
| <Button variant={commitVariant} size="sm" onClick={commit}> | ||
| {commitLabel} | ||
| </Button> |
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| disabled={disabled} | ||
| onClick={() => setRouteDialogOpen(true)} | ||
| onMouseEnter={handleAim} | ||
| onFocus={handleAim} | ||
| onMouseLeave={handleClearAim} | ||
| onBlur={handleClearAim} | ||
| > |
| <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"; |
| 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); | ||
| } |
| server.listen(LISTENER_PORT, () => | ||
| log( | ||
| `HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`, | ||
| ), | ||
| ); |
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>
…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>
5535b9f to
df828b4
Compare
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>
df828b4 to
8921124
Compare
There was a problem hiding this comment.
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 demobrittle 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)`,
),
);
| 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", | ||
| { |
| 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", |
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
There was a problem hiding this comment.
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).
| 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 { |
| 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 { |
| 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); | ||
| }; |
There was a problem hiding this comment.
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:resetscripts hardcodeapps/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 pinnedconcurrentlyvia pnpm so the script works from a clean checkout without relying on another subproject’snode_modules.
apps/apollo-vertex/package.json:10 - This
demoscript depends onslack/node_modules/.bin/concurrently, which is brittle and breaks ifapps/apollo-vertex/slackhasn’t beennpm install’d yet. Using a pinned pnpm dlx (or addingconcurrentlyto this package’s devDependencies and usingpnpm 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",
| "use client"; | ||
| import { InvoiceReviewTemplate } from "@/templates/invoice-review/InvoiceReviewTemplate"; |
| <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" | ||
| /> |
| <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" | ||
| /> |
| <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" | ||
| /> |
| <textarea | ||
| ref={bodyRef} | ||
| value={body} | ||
| onChange={(e) => { | ||
| setBody(e.target.value); |
| <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>
3d7bcf1 to
3aed9d7
Compare
There was a problem hiding this comment.
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-pointeris applied unconditionally, so disabled buttons (oraria-disabledbuttons) can still show a pointer cursor. Preferenabled:cursor-pointerso 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
handleSendClickschedules nestedsetTimeoutcalls but doesn't clear them on close/unmount. If the dialog closes quickly, this can set state on an unmounted component and can also callonSendafter discard. Track timeout IDs in refs andclearTimeoutthem 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 viaaria-labelledby) so screen readers can announce it properly.
| if (i18n.isInitialized) { | ||
| document.documentElement.lang = i18n.language; | ||
| return; | ||
| } |
| 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>
There was a problem hiding this comment.
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 demofail 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 -- concurrentlykeeps 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 emptyreasonwhenchipsis 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 setCursorstores an empty string when clearing the cursor (exceptionId undefined). That meansgetCursor()can return "" (which doesn’t match itsstring | undefinedcontract) and also leaves a permanent key incursorMapeven when no cursor exists. Prefer deleting the key (or storing undefined) when clearing.
| <Button | ||
| aria-disabled | ||
| onClick={(e) => e.preventDefault()} | ||
| className={cn("opacity-50", "cursor-not-allowed")} | ||
| > | ||
| Approve | ||
| </Button> |
No description provided.