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:
+54
@@ -177,6 +177,10 @@ export default function App() {
|
||||
// gate the turn flow ("Jump forward" stays enabled while the advisor thinks) —
|
||||
// it only gates the advisor UI (the "Ask advisor" button + its hint).
|
||||
const [advisorBusy, setAdvisorBusy] = useState(false);
|
||||
// Separate again: a pending nation-op confirm/reject is its own (fast,
|
||||
// non-LLM) request. It gates only the nation-op dialog controls, never the
|
||||
// turn flow — "Jump forward" stays usable while it is in flight.
|
||||
const [opBusy, setOpBusy] = useState(false);
|
||||
// The id of the game the user is/was last in (mirrors localStorage) and the
|
||||
// list of saved games (newest first) shown on the home screen.
|
||||
const [activeGameId, setActiveGameId] = useState<string | null>(() => readActiveGameId());
|
||||
@@ -401,6 +405,43 @@ export default function App() {
|
||||
[game, applyState],
|
||||
);
|
||||
|
||||
// Apply a pending nation op the model proposed (confirm, with an optional
|
||||
// rename-name edit). Fast, non-LLM — gated by `opBusy`, not `busy`. The
|
||||
// version-bumped state comes back in the response and the SSE `state` event.
|
||||
const runConfirmNationOp = useCallback(
|
||||
async (index: number, edits?: { name?: string }) => {
|
||||
if (!game) return;
|
||||
setOpBusy(true);
|
||||
try {
|
||||
const { game: g } = await api.confirmNationOp(game.id, index, edits);
|
||||
applyState(g);
|
||||
toast.success('Nation change applied');
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
setOpBusy(false);
|
||||
}
|
||||
},
|
||||
[game, applyState],
|
||||
);
|
||||
|
||||
// Drop a pending nation op (reject / dismiss). Fast, non-LLM — `opBusy`.
|
||||
const runRejectNationOp = useCallback(
|
||||
async (index: number) => {
|
||||
if (!game) return;
|
||||
setOpBusy(true);
|
||||
try {
|
||||
const { game: g } = await api.rejectNationOp(game.id, index);
|
||||
applyState(g);
|
||||
} catch (e) {
|
||||
toastError(e);
|
||||
} finally {
|
||||
setOpBusy(false);
|
||||
}
|
||||
},
|
||||
[game, applyState],
|
||||
);
|
||||
|
||||
// Jump forward: the single LLM call that commits the pending round. Only this
|
||||
// sets `busy` (the adds above are fast and non-LLM).
|
||||
const runJump = useCallback(async () => {
|
||||
@@ -507,12 +548,15 @@ export default function App() {
|
||||
regions={regions}
|
||||
busy={busy}
|
||||
advisorBusy={advisorBusy}
|
||||
opBusy={opBusy}
|
||||
advisors={advisors}
|
||||
onAddAction={runAddAction}
|
||||
onAddDiplomacy={runAddDiplomacy}
|
||||
onRemoveQueueItem={runRemoveQueueItem}
|
||||
onAdvisor={askAdvisor}
|
||||
onJump={runJump}
|
||||
onConfirmNationOp={runConfirmNationOp}
|
||||
onRejectNationOp={runRejectNationOp}
|
||||
onHome={goHome}
|
||||
/>
|
||||
</>
|
||||
@@ -551,12 +595,16 @@ interface GameScreenProps {
|
||||
busy: boolean;
|
||||
/** An advisor fetch is in flight — drives the advisor UI only (never the turn). */
|
||||
advisorBusy: boolean;
|
||||
/** A nation-op confirm/reject is in flight — drives the dialog controls only. */
|
||||
opBusy: boolean;
|
||||
advisors: string[];
|
||||
onAddAction: (a: string) => void;
|
||||
onAddDiplomacy: (to: string, m: string) => void;
|
||||
onRemoveQueueItem: (index: number) => void;
|
||||
onAdvisor: (p?: string) => void;
|
||||
onJump: () => void;
|
||||
onConfirmNationOp: (index: number, edits?: { name?: string }) => void;
|
||||
onRejectNationOp: (index: number) => void;
|
||||
onHome: () => void;
|
||||
}
|
||||
|
||||
@@ -579,12 +627,15 @@ function GameScreen({
|
||||
regions,
|
||||
busy,
|
||||
advisorBusy,
|
||||
opBusy,
|
||||
advisors,
|
||||
onAddAction,
|
||||
onAddDiplomacy,
|
||||
onRemoveQueueItem,
|
||||
onAdvisor,
|
||||
onJump,
|
||||
onConfirmNationOp,
|
||||
onRejectNationOp,
|
||||
onHome,
|
||||
}: GameScreenProps) {
|
||||
// Which nation's dossier to show. Defaults to the player; remounts per game.
|
||||
@@ -835,12 +886,15 @@ function GameScreen({
|
||||
game={game}
|
||||
busy={busy}
|
||||
advisorBusy={advisorBusy}
|
||||
opBusy={opBusy}
|
||||
advisors={advisors}
|
||||
onAddAction={onAddAction}
|
||||
onAddDiplomacy={onAddDiplomacy}
|
||||
onRemoveQueueItem={onRemoveQueueItem}
|
||||
onAdvisor={onAdvisor}
|
||||
onJump={onJump}
|
||||
onConfirmNationOp={onConfirmNationOp}
|
||||
onRejectNationOp={onRejectNationOp}
|
||||
inWindow
|
||||
/>
|
||||
</FloatingWindow>
|
||||
|
||||
Reference in New Issue
Block a user