Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/dashboard/app/(tenant)/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import Link from "next/link";
import { CopyPill } from "@ainfera/ui";
import { Crumbs } from "@/components/v15/Crumbs";
import { getOwnerHandle } from "@/lib/ainfera/proxy";
import { listAgents } from "@/lib/v15/api";

Expand All @@ -13,6 +14,14 @@ export default async function AgentsPage() {

return (
<>
{/* AIN-251 W4 · shared <Crumbs /> for cross-page parity. */}
<Crumbs
path={[
{ label: "workspace", href: "/dashboard" },
{ label: "agents" },
]}
/>

<div className="title-row">
<div>
<h1>Agents</h1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,74 @@ export function InferenceDetailClient({ inferenceId, initial }: Props) {
) : null}
</div>

{/* AIN-266 W3 · candidates card — the routing decision artifact.
* Surfaces every model Ainfera Inference considered for this call,
* the rank order, q_prior, projected cost, and drop reasons.
* Renders only when the api returned a non-empty candidates list
* (null on pre-§16 rows + pinned-passthroughs that bypass the
* brain). Authed-tenant-only on the wire (§D3 lock). */}
{inf.candidates && inf.candidates.length > 0 ? (
<div className="rounded-lg border border-hairline-strong bg-bg-elev">
<div className="flex items-center justify-between border-b border-hairline-strong px-6 py-3">
<p className="font-mono text-xs uppercase tracking-[0.15em] text-ink-muted">
Candidates ({inf.candidates.length})
</p>
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-ink-faint">
the decision · ranked
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full font-mono text-xs">
<thead className="border-b border-hairline">
<tr className="text-ink-faint">
<th className="px-6 py-2 text-left font-normal uppercase tracking-chip text-[10px]">#</th>
<th className="px-3 py-2 text-left font-normal uppercase tracking-chip text-[10px]">Model</th>
<th className="px-3 py-2 text-left font-normal uppercase tracking-chip text-[10px]">Brand</th>
<th className="px-3 py-2 text-right font-normal uppercase tracking-chip text-[10px]">q_prior</th>
<th className="px-3 py-2 text-right font-normal uppercase tracking-chip text-[10px]">proj. cost</th>
<th className="px-3 py-2 text-left font-normal uppercase tracking-chip text-[10px]">eligible</th>
<th className="px-6 py-2 text-left font-normal uppercase tracking-chip text-[10px]">drop reason</th>
</tr>
</thead>
<tbody>
{inf.candidates.map((c, i) => {
const chosen = c.model_slug === inf.model_used;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Null-null match gives false "chosen" badge

Low Severity

The comparison c.model_slug === inf.model_used evaluates to true when both values are null (null === null), which would incorrectly render the "chosen" badge on a candidate row. Both model_slug (string | null) and model_used (string | null) allow null. Adding a null guard like inf.model_used != null && c.model_slug === inf.model_used prevents a misleading "chosen" highlight when no model was actually selected.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bf414c. Configure here.

return (
<tr
key={`${c.model_id ?? c.model_slug ?? i}-${i}`}
className={
chosen
? "bg-accent/5 border-l-2 border-accent"
: "hover:bg-bg-card"
}
>
<td className="px-6 py-2 text-ink-muted">{c.rank ?? i + 1}</td>
<td className="px-3 py-2 text-ink">
{c.model_slug ?? "—"}
{chosen ? (
<span className="ml-2 rounded bg-accent/15 px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-chip text-accent">
chosen
</span>
) : null}
</td>
<td className="px-3 py-2 text-ink-muted">{c.brand_slug ?? "—"}</td>
<td className="px-3 py-2 text-right text-ink">{c.q_prior ?? "—"}</td>
<td className="px-3 py-2 text-right text-ink">
{c.projected_cost_usd ? fmtCost(c.projected_cost_usd) : "—"}
</td>
<td className="px-3 py-2 text-ink-muted">
{c.m_allowed === true ? "yes" : c.m_allowed === false ? "no" : "—"}
</td>
<td className="px-6 py-2 text-ink-muted">{c.drop_reason ?? "—"}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : null}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Candidates card added to component never rendered

Medium Severity

The new candidates card JSX (~65 lines) and InferenceCandidate type are added to InferenceDetailClient, which is exported but never imported or rendered by any page. The actual /inferences/[id]/page.tsx renders its own inline JSX using getInference (returning InferenceRecord, not InferenceDetail). The AIN-266 candidates card feature claimed in the PR description won't be visible to users — the entire addition is dead code.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bf414c. Configure here.


{/* Audit chain block — AIN-182 §4 "Verify with curl" requirement. */}
<div className="rounded-lg border border-hairline-strong bg-bg-elev">
<div className="flex items-center justify-between border-b border-hairline-strong px-6 py-3">
Expand Down
21 changes: 21 additions & 0 deletions apps/dashboard/app/(tenant)/inferences/[id]/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,25 @@ export type InferenceDetail = {
policy_version?: string | null;
cell?: string | null;
routing_rationale?: string | null;
/** AIN-266 W3 · routing candidate set. Mirrors §16
* routing_outcomes.candidates JSONB. NULL on pre-2026-05-21 rows +
* pinned-passthrough calls. AUTHED-tenant-only on the wire (§D3 lock
* — never on /v1/audit/public). */
candidates?: InferenceCandidate[] | null;
};

/** One row of the §16 candidate set. Keys mirror the JSONB shape on
* routing_outcomes exactly. Decimals come over as strings to preserve
* precision through JSON. */
export type InferenceCandidate = {
model_id?: string | null;
model_slug?: string | null;
brand_slug?: string | null;
q_prior?: string | null;
price_in_per_mtok_usd?: string | null;
price_out_per_mtok_usd?: string | null;
m_allowed?: boolean | null;
projected_cost_usd?: string | null;
drop_reason?: string | null;
rank?: number | null;
};
18 changes: 9 additions & 9 deletions apps/dashboard/app/(tenant)/inferences/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Crumbs } from "@/components/v15/Crumbs";
import { getOwnerHandle } from "@/lib/ainfera/proxy";
import { publicAuditFeed, publicLeaderboard } from "@/lib/v15/api";

Expand All @@ -25,15 +26,14 @@ export default async function InferencesIndexPage() {

return (
<>
<div className="crumbs">
<div className="l">
<div className="path">
<Link href="/dashboard">workspace</Link>
<span className="sep">/</span>
<span className="here">inferences</span>
</div>
</div>
</div>
{/* AIN-251 W4 · shared <Crumbs /> for cross-page parity. */}
<Crumbs
path={[
{ label: "workspace", href: "/dashboard" },
{ label: "inferences" },
]}
/>


<div className="title-row">
<div>
Expand Down
9 changes: 9 additions & 0 deletions apps/dashboard/app/(tenant)/workflows/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Crumbs } from "@/components/v15/Crumbs";
import { getOwnerHandle } from "@/lib/ainfera/proxy";
import { listWorkflows } from "@/lib/v15/api";

Expand All @@ -12,6 +13,14 @@ export default async function WorkflowsPage() {

return (
<>
{/* AIN-251 W4 · shared <Crumbs /> for cross-page parity. */}
<Crumbs
path={[
{ label: "workspace", href: "/dashboard" },
{ label: "workflows" },
]}
/>

<div className="title-row">
<div>
<h1>Workflows</h1>
Expand Down
Loading