feat(giellie): show stock quantity approvals as a table
A 40-line stocktake in raw JSON is unreviewable. The stock quantity tool gets a table with the mode, SKU, description and quantity. Every other tool keeps its JSON exactly as it was.
This commit is contained in:
parent
5c106cf033
commit
0db696c898
2 changed files with 133 additions and 3 deletions
|
|
@ -94,3 +94,82 @@ describe("ApprovalCard", () => {
|
|||
expect((screen.getByRole("button", { name: /reject/i }) as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("the stock quantity tool renders a table", () => {
|
||||
it("lists the sku, description and qty", () => {
|
||||
render(
|
||||
<ApprovalCard
|
||||
pending={{
|
||||
tool_uses: [
|
||||
{
|
||||
id: "w1",
|
||||
name: "stock_items.set_quantities",
|
||||
input: {
|
||||
mode: "set",
|
||||
lines: [
|
||||
{ sku: "ABC-123", description: "Bolt M12", qty: 50 },
|
||||
{ sku: "BOLT-6MM", description: "Bolt M6", qty: 120 },
|
||||
],
|
||||
},
|
||||
confirmAlways: false,
|
||||
},
|
||||
],
|
||||
completed: [],
|
||||
order: ["w1"],
|
||||
}}
|
||||
onDecide={vi.fn()}
|
||||
busy={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("ABC-123")).toBeTruthy();
|
||||
expect(screen.getByText("Bolt M12")).toBeTruthy();
|
||||
expect(screen.getByText("50")).toBeTruthy();
|
||||
expect(screen.getByText(/new quantity on hand/i)).toBeTruthy();
|
||||
expect(document.body.textContent).not.toMatch(/"lines"/);
|
||||
});
|
||||
|
||||
it("says when the quantities are being added instead of set", () => {
|
||||
render(
|
||||
<ApprovalCard
|
||||
pending={{
|
||||
tool_uses: [
|
||||
{
|
||||
id: "w1",
|
||||
name: "stock_items.set_quantities",
|
||||
input: { mode: "add", lines: [{ sku: "ABC-123", qty: 50 }] },
|
||||
confirmAlways: false,
|
||||
},
|
||||
],
|
||||
completed: [],
|
||||
order: ["w1"],
|
||||
}}
|
||||
onDecide={vi.fn()}
|
||||
busy={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/adding these quantities/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("leaves other tools as JSON", () => {
|
||||
render(
|
||||
<ApprovalCard
|
||||
pending={{
|
||||
tool_uses: [
|
||||
{
|
||||
id: "w1",
|
||||
name: "orders.update",
|
||||
input: { id: "abc", status: "approved" },
|
||||
confirmAlways: false,
|
||||
},
|
||||
],
|
||||
completed: [],
|
||||
order: ["w1"],
|
||||
}}
|
||||
onDecide={vi.fn()}
|
||||
busy={false}
|
||||
/>,
|
||||
);
|
||||
expect(document.body.textContent).toMatch(/"status"/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import { Check, Mail, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { PendingAction } from "@/lib/giellie-types";
|
||||
|
||||
type StockQuantityLine = { sku?: string; description?: string; qty?: number };
|
||||
|
||||
/**
|
||||
* The stock quantity tool is the one tool whose input is a list the
|
||||
* operator has to read line by line. JSON is not reviewable at that
|
||||
* size, so it gets a table. Every other tool keeps raw JSON.
|
||||
*/
|
||||
export function renderToolInput(toolName: string, input: unknown): ReactNode {
|
||||
if (toolName === "stock_items.set_quantities" && input && typeof input === "object") {
|
||||
const payload = input as { mode?: unknown; lines?: unknown };
|
||||
const mode = typeof payload.mode === "string" ? payload.mode : "";
|
||||
const lines = Array.isArray(payload.lines) ? (payload.lines as StockQuantityLine[]) : [];
|
||||
if (lines.length > 0) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{mode === "add"
|
||||
? "Adding these quantities to the current stock."
|
||||
: "Setting these as the new quantity on hand."}
|
||||
</p>
|
||||
<div className="max-h-64 overflow-auto rounded-md border">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-2 py-1 text-left font-medium">SKU</th>
|
||||
<th className="px-2 py-1 text-left font-medium">Description</th>
|
||||
<th className="px-2 py-1 text-right font-medium">Qty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines.map((line, i) => (
|
||||
<tr key={`${line.sku ?? "line"}-${i}`} className="border-t">
|
||||
<td className="px-2 py-1 font-mono">{line.sku ?? "—"}</td>
|
||||
<td className="px-2 py-1">{line.description ?? "—"}</td>
|
||||
<td className="px-2 py-1 text-right tabular-nums">{line.qty ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
// Every other tool keeps its raw JSON exactly as it was.
|
||||
return (
|
||||
<pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words text-xs text-muted-foreground">
|
||||
{JSON.stringify(input, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
pending: PendingAction;
|
||||
busy: boolean;
|
||||
|
|
@ -39,9 +92,7 @@ export function ApprovalCard({ pending, busy, onDecide }: Props) {
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words text-xs text-muted-foreground">
|
||||
{JSON.stringify(tu.input, null, 2)}
|
||||
</pre>
|
||||
{renderToolInput(tu.name, tu.input)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
Loading…
Reference in a new issue