feat: nation-op rename review — confirm/reject routes, client dialog, AGENTS.md (P4a, #16)

Wire the 'LLM proposes, player confirms' nation-rename flow end to end:
- server: POST /api/games/:id/nation-op/confirm|reject (404/400, version-bumped SSE state)
- client: confirmNationOp/rejectNationOp api helpers + NationOpDialog (editable rename) wired into the Action panel via a 'Nation proposals' trigger
- AGENTS.md: document the pendingNationOps mechanism, routes, nationRef resolution, stub-never-proposes
This commit is contained in:
2026-10-14 13:00:28 +00:00
parent e9fa267fd0
commit 9f230606c4
7 changed files with 335 additions and 12 deletions
+42 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Loader2, MessageSquare, Radar, Trash2, Zap } from 'lucide-react';
import { Loader2, MessageSquare, PenLine, Radar, Trash2, Zap } from 'lucide-react';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
@@ -15,6 +15,7 @@ import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import NationOpDialog from './NationOpDialog';
import type { GameState, RoundItem } from '@shared/types';
interface Props {
@@ -27,6 +28,11 @@ interface Props {
* never disable "Jump forward" or the rest of the turn flow.
*/
advisorBusy: boolean;
/**
* A pending nation-op confirm/reject request is in flight. Distinct from
* `busy`: it gates only the nation-op dialog controls, never the turn flow.
*/
opBusy: boolean;
/** Advisor brainstorm suggestions from the last request. */
advisors: string[];
/** Queue a free-text action onto the pending round (no turn advance). */
@@ -38,6 +44,10 @@ interface Props {
onAdvisor: (prompt?: string) => void;
/** Commit the whole pending round in a single LLM turn. */
onJump: () => void;
/** Apply a pending nation op; `edits.name` optionally overrides a rename. */
onConfirmNationOp: (index: number, edits?: { name?: string }) => void;
/** Drop (reject) a pending nation op by its index. */
onRejectNationOp: (index: number) => void;
/**
* Render without the outer card chrome (the surrounding floating window
* provides the border and the "Your move" title). Same content, same testids.
@@ -66,12 +76,15 @@ export default function ActionPanel({
game,
busy,
advisorBusy,
opBusy,
advisors,
onAddAction,
onAddDiplomacy,
onRemoveQueueItem,
onAdvisor,
onJump,
onConfirmNationOp,
onRejectNationOp,
inWindow = false,
}: Props) {
const [tab, setTab] = useState('action');
@@ -81,6 +94,8 @@ export default function ActionPanel({
const [prompt, setPrompt] = useState('');
// Index of the queued item the user is about to remove, or null (dialog closed).
const [confirmRemoveIndex, setConfirmRemoveIndex] = useState<number | null>(null);
// Whether the nation-op review dialog is open.
const [opDialogOpen, setOpDialogOpen] = useState(false);
const others = Object.entries(game.nations)
.filter(([id]) => id !== game.playerNationId)
@@ -108,6 +123,7 @@ export default function ActionPanel({
};
const pending = game.pending;
const ops = game.pendingNationOps;
// The item the confirmation dialog is about (re-derived from the live list,
// so if the queue changes while the dialog is open it tracks the current
// item at that index — or the dialog closes if the index went out of range).
@@ -325,6 +341,31 @@ export default function ActionPanel({
</DialogContent>
</Dialog>
{/* Pending nation ops proposed by the model — open to confirm or reject. */}
{ops.length > 0 && (
<button
type="button"
onClick={() => setOpDialogOpen(true)}
disabled={busy}
data-testid="nation-op-trigger"
className="flex w-full items-center justify-between gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-2.5 text-sm hover:bg-amber-500/15"
>
<span className="flex items-center gap-2 font-medium text-amber-300">
<PenLine className="size-4" />
{ops.length} nation {ops.length === 1 ? 'proposal' : 'proposals'} — review
</span>
<span className="text-xs text-amber-300/70">confirm or reject</span>
</button>
)}
<NationOpDialog
game={game}
open={opDialogOpen}
onOpenChange={setOpDialogOpen}
opBusy={opBusy}
onConfirm={onConfirmNationOp}
onReject={onRejectNationOp}
/>
<Button className="h-11 w-full" onClick={onJump} disabled={busy} data-testid="jump-forward">
{busy ? <Loader2 className="size-4 animate-spin" /> : <Radar className="size-4" />}
{queuedLabel}