From daf1cb9bb160c165af45eb21a4e51fc2d7030532 Mon Sep 17 00:00:00 2001 From: Shironex Date: Thu, 1 Oct 2026 22:15:02 +0200 Subject: [PATCH 1/5] refactor(web): remove the council view and its bridge wrappers --- .../components/app/AppShell/AppShell.types.ts | 5 - .../components/app/AppShell/AppShellViews.tsx | 18 - .../components/app/AppShell/nav.constants.tsx | 13 +- .../ConvergeGavel/ConvergeGavel.hooks.ts | 133 ------- .../ConvergeGavel/ConvergeGavel.stories.tsx | 47 --- .../ConvergeGavel/ConvergeGavel.test.tsx | 87 ---- .../council/ConvergeGavel/ConvergeGavel.tsx | 195 --------- .../ConvergeGavel/ConvergeGavel.types.ts | 30 -- .../components/council/ConvergeGavel/index.ts | 6 - .../CouncilReplay/CouncilReplay.hooks.ts | 92 ----- .../CouncilReplay/CouncilReplay.stories.tsx | 88 ----- .../CouncilReplay/CouncilReplay.test.tsx | 98 ----- .../council/CouncilReplay/CouncilReplay.tsx | 73 ---- .../CouncilReplay/CouncilReplay.types.ts | 12 - .../components/council/CouncilReplay/index.ts | 2 - .../CouncilStartPanel.hooks.ts | 64 --- .../CouncilStartPanel.stories.tsx | 17 - .../CouncilStartPanel.test.tsx | 71 ---- .../CouncilStartPanel/CouncilStartPanel.tsx | 120 ------ .../CouncilStartPanel.types.ts | 11 - .../council/CouncilStartPanel/index.ts | 2 - .../council/CouncilView/CouncilView.hooks.ts | 372 ------------------ .../CouncilView/CouncilView.stories.tsx | 35 -- .../council/CouncilView/CouncilView.test.tsx | 64 --- .../council/CouncilView/CouncilView.tsx | 146 ------- .../council/CouncilView/CouncilView.types.ts | 8 - .../components/council/CouncilView/index.ts | 2 - .../HumanInputBar/HumanInputBar.hooks.ts | 119 ------ .../HumanInputBar/HumanInputBar.stories.tsx | 34 -- .../HumanInputBar/HumanInputBar.test.tsx | 98 ----- .../council/HumanInputBar/HumanInputBar.tsx | 177 --------- .../HumanInputBar/HumanInputBar.types.ts | 24 -- .../components/council/HumanInputBar/index.ts | 2 - .../council/ReplyDiff/ReplyDiff.hooks.ts | 5 - .../council/ReplyDiff/ReplyDiff.stories.tsx | 93 ----- .../council/ReplyDiff/ReplyDiff.test.tsx | 49 --- .../council/ReplyDiff/ReplyDiff.tsx | 90 ----- .../council/ReplyDiff/ReplyDiff.types.ts | 8 - .../src/components/council/ReplyDiff/index.ts | 2 - .../council/SeatCanvas/SeatCanvas.hooks.ts | 5 - .../council/SeatCanvas/SeatCanvas.stories.tsx | 55 --- .../council/SeatCanvas/SeatCanvas.test.tsx | 114 ------ .../council/SeatCanvas/SeatCanvas.tsx | 134 ------- .../council/SeatCanvas/SeatCanvas.types.ts | 18 - .../components/council/SeatCanvas/index.ts | 2 - .../council/TeamChat/TeamChat.hooks.ts | 61 --- .../council/TeamChat/TeamChat.stories.tsx | 24 -- .../council/TeamChat/TeamChat.test.tsx | 23 -- .../components/council/TeamChat/TeamChat.tsx | 115 ------ .../council/TeamChat/TeamChat.types.ts | 7 - .../src/components/council/TeamChat/index.ts | 2 - .../src/components/council/council-presets.ts | 47 --- .../components/council/council-replay.test.ts | 49 --- .../src/components/council/council-replay.ts | 43 -- .../src/components/council/council-roles.ts | 19 - .../council/council-transcript.test.tsx | 118 ------ .../components/council/council-transcript.ts | 129 ------ .../src/components/council/council.types.ts | 125 ------ apps/web/src/components/council/index.ts | 4 - .../components/council/reply-diff.test.tsx | 89 ----- apps/web/src/components/council/reply-diff.ts | 124 ------ apps/web/src/lib/bridge.test.tsx | 79 ---- apps/web/src/lib/bridge/commands.ts | 1 - apps/web/src/lib/bridge/commands/council.ts | 109 ----- apps/web/src/lib/bridge/events.narrowers.ts | 14 - apps/web/src/lib/bridge/events.ts | 13 - apps/web/src/lib/bridge/events.types.ts | 8 - apps/web/src/lib/bridge/types.ts | 20 - 68 files changed, 2 insertions(+), 4061 deletions(-) delete mode 100644 apps/web/src/components/council/ConvergeGavel/ConvergeGavel.hooks.ts delete mode 100644 apps/web/src/components/council/ConvergeGavel/ConvergeGavel.stories.tsx delete mode 100644 apps/web/src/components/council/ConvergeGavel/ConvergeGavel.test.tsx delete mode 100644 apps/web/src/components/council/ConvergeGavel/ConvergeGavel.tsx delete mode 100644 apps/web/src/components/council/ConvergeGavel/ConvergeGavel.types.ts delete mode 100644 apps/web/src/components/council/ConvergeGavel/index.ts delete mode 100644 apps/web/src/components/council/CouncilReplay/CouncilReplay.hooks.ts delete mode 100644 apps/web/src/components/council/CouncilReplay/CouncilReplay.stories.tsx delete mode 100644 apps/web/src/components/council/CouncilReplay/CouncilReplay.test.tsx delete mode 100644 apps/web/src/components/council/CouncilReplay/CouncilReplay.tsx delete mode 100644 apps/web/src/components/council/CouncilReplay/CouncilReplay.types.ts delete mode 100644 apps/web/src/components/council/CouncilReplay/index.ts delete mode 100644 apps/web/src/components/council/CouncilStartPanel/CouncilStartPanel.hooks.ts delete mode 100644 apps/web/src/components/council/CouncilStartPanel/CouncilStartPanel.stories.tsx delete mode 100644 apps/web/src/components/council/CouncilStartPanel/CouncilStartPanel.test.tsx delete mode 100644 apps/web/src/components/council/CouncilStartPanel/CouncilStartPanel.tsx delete mode 100644 apps/web/src/components/council/CouncilStartPanel/CouncilStartPanel.types.ts delete mode 100644 apps/web/src/components/council/CouncilStartPanel/index.ts delete mode 100644 apps/web/src/components/council/CouncilView/CouncilView.hooks.ts delete mode 100644 apps/web/src/components/council/CouncilView/CouncilView.stories.tsx delete mode 100644 apps/web/src/components/council/CouncilView/CouncilView.test.tsx delete mode 100644 apps/web/src/components/council/CouncilView/CouncilView.tsx delete mode 100644 apps/web/src/components/council/CouncilView/CouncilView.types.ts delete mode 100644 apps/web/src/components/council/CouncilView/index.ts delete mode 100644 apps/web/src/components/council/HumanInputBar/HumanInputBar.hooks.ts delete mode 100644 apps/web/src/components/council/HumanInputBar/HumanInputBar.stories.tsx delete mode 100644 apps/web/src/components/council/HumanInputBar/HumanInputBar.test.tsx delete mode 100644 apps/web/src/components/council/HumanInputBar/HumanInputBar.tsx delete mode 100644 apps/web/src/components/council/HumanInputBar/HumanInputBar.types.ts delete mode 100644 apps/web/src/components/council/HumanInputBar/index.ts delete mode 100644 apps/web/src/components/council/ReplyDiff/ReplyDiff.hooks.ts delete mode 100644 apps/web/src/components/council/ReplyDiff/ReplyDiff.stories.tsx delete mode 100644 apps/web/src/components/council/ReplyDiff/ReplyDiff.test.tsx delete mode 100644 apps/web/src/components/council/ReplyDiff/ReplyDiff.tsx delete mode 100644 apps/web/src/components/council/ReplyDiff/ReplyDiff.types.ts delete mode 100644 apps/web/src/components/council/ReplyDiff/index.ts delete mode 100644 apps/web/src/components/council/SeatCanvas/SeatCanvas.hooks.ts delete mode 100644 apps/web/src/components/council/SeatCanvas/SeatCanvas.stories.tsx delete mode 100644 apps/web/src/components/council/SeatCanvas/SeatCanvas.test.tsx delete mode 100644 apps/web/src/components/council/SeatCanvas/SeatCanvas.tsx delete mode 100644 apps/web/src/components/council/SeatCanvas/SeatCanvas.types.ts delete mode 100644 apps/web/src/components/council/SeatCanvas/index.ts delete mode 100644 apps/web/src/components/council/TeamChat/TeamChat.hooks.ts delete mode 100644 apps/web/src/components/council/TeamChat/TeamChat.stories.tsx delete mode 100644 apps/web/src/components/council/TeamChat/TeamChat.test.tsx delete mode 100644 apps/web/src/components/council/TeamChat/TeamChat.tsx delete mode 100644 apps/web/src/components/council/TeamChat/TeamChat.types.ts delete mode 100644 apps/web/src/components/council/TeamChat/index.ts delete mode 100644 apps/web/src/components/council/council-presets.ts delete mode 100644 apps/web/src/components/council/council-replay.test.ts delete mode 100644 apps/web/src/components/council/council-replay.ts delete mode 100644 apps/web/src/components/council/council-roles.ts delete mode 100644 apps/web/src/components/council/council-transcript.test.tsx delete mode 100644 apps/web/src/components/council/council-transcript.ts delete mode 100644 apps/web/src/components/council/council.types.ts delete mode 100644 apps/web/src/components/council/index.ts delete mode 100644 apps/web/src/components/council/reply-diff.test.tsx delete mode 100644 apps/web/src/components/council/reply-diff.ts delete mode 100644 apps/web/src/lib/bridge/commands/council.ts diff --git a/apps/web/src/components/app/AppShell/AppShell.types.ts b/apps/web/src/components/app/AppShell/AppShell.types.ts index 36687680..eb4d5cf3 100644 --- a/apps/web/src/components/app/AppShell/AppShell.types.ts +++ b/apps/web/src/components/app/AppShell/AppShell.types.ts @@ -12,11 +12,6 @@ export type AppView = // stage shells, it isn't a provenance target) — so `nav-render-parity` is // unaffected, but its render branch lands in the SAME commit as this member. | 'history' - // The Council debate canvas (issue #352) — a governed multi-agent debate board. A - // Project-group destination, NOT a source-ref REGISTRY view (a council run has no - // provenance token), so `nav-render-parity` is unaffected; its render branch lands - // in the SAME commit as this member. - | 'council' // The five stage destinations (Phase-1 view rethink, PR 3): Understand hosts // Insight's Find + Scorecard's Grade behind one shell; Harden / Enforce are two // view filters over the ONE HarnessView run/store; PR Review + Issue Triage keep diff --git a/apps/web/src/components/app/AppShell/AppShellViews.tsx b/apps/web/src/components/app/AppShell/AppShellViews.tsx index 798e119a..b482990b 100644 --- a/apps/web/src/components/app/AppShell/AppShellViews.tsx +++ b/apps/web/src/components/app/AppShell/AppShellViews.tsx @@ -51,12 +51,6 @@ const TerminalView = lazy(() => const HistoryView = lazy(() => import('@/components/history').then((m) => ({ default: m.HistoryView })), ); -// The Council canvas (issue #352) is a heavy, rarely-first-paint surface (its own -// seat-grid + team-chat + Markdown rendering), so it is code-split behind its own lazy -// chunk like the other Project-group destinations. -const CouncilView = lazy(() => - import('@/components/council').then((m) => ({ default: m.CouncilView })), -); /** Map a History row (family + run) to a run-level provenance target: Insight and * Scorecard both land on the Understand stage (its shell splits by `family`), @@ -260,18 +254,6 @@ export function AppShellViews({ standalone `insight` / `scorecard` routes were removed in the flip — legacy `insight:` / `scorecard:` provenance tokens now retarget here through the source-ref REGISTRY. */} - {/* Council debate canvas (issue #352): a governed multi-agent debate board — - the seat-node grid beside the team-chat projection of the nc:debate bus. - Reads the active project root off the registry (seats debate over it). */} - {view === 'council' && ( - }> - - - )} - {view === 'understand' && ( }> , group: 'project', }, - { - view: 'council', - label: 'Council', - hint: 'C', - icon: , - group: 'project', - }, { view: 'issuetriage', label: STAGE_BY_ID.intake.destination, diff --git a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.hooks.ts b/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.hooks.ts deleted file mode 100644 index 3f9ae690..00000000 --- a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.hooks.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Local form state for the {@link import('./ConvergeGavel').ConvergeGavel} (issue #353): - * the seat selected for an `accept`, the ruling/reason note, WHICH verdict is in flight, - * the reject confirmation, and the dispatch error. Kept out of the component body - * (no-state-in-body); the three verdicts (`accept` / `reject` / `judge`) map to one - * mediated resolve call. Tracking the in-flight decision (not a bare boolean) lets only - * the PRESSED button show busy while the others merely disable (GOV-8), and reject routes - * through a confirm dialog first (GOV-9). - */ -import { useCallback, useState } from 'react'; - -import type { CouncilConvergeDecision } from '@/lib/bridge'; - -import type { - ConvergeResolve, - ConvergeResolveOptions, -} from './ConvergeGavel.types'; - -export interface ConvergeGavelModel { - /** The seat whose position an `accept` would adopt, or `null` when none is picked. */ - selectedSeatId: string | null; - /** Pick the seat to adopt. */ - select: (seatId: string) => void; - /** The ruling (for `judge`) or an optional reason (for `accept`/`reject`). */ - note: string; - setNote: (value: string) => void; - /** The verdict currently being dispatched, or `null` when idle — the PRESSED button - * reads busy from this while the others merely disable (GOV-8). */ - pending: CouncilConvergeDecision | null; - /** True while any verdict dispatch is in flight — disables the controls. */ - busy: boolean; - /** The last dispatch failure, shown inline so the human can retry. */ - error: string | null; - /** Whether an `accept` can be submitted (a seat is selected). */ - canAccept: boolean; - /** Whether a `judge` ruling can be submitted (the note is non-empty). */ - canJudge: boolean; - /** Adopt the selected seat's position. */ - accept: () => void; - /** Whether the reject confirmation dialog is open (GOV-9). */ - rejectConfirmOpen: boolean; - /** Open the reject confirmation — rejecting closes the run, so it is guarded. */ - requestReject: () => void; - /** Dismiss the reject confirmation (no-op mid-dispatch). */ - closeRejectConfirm: () => void; - /** Reject every position — the run closes with no adopted outcome (dialog-confirmed). */ - confirmReject: () => void; - /** Record the human's own ruling (the note). */ - judge: () => void; - /** ⌘/Ctrl+↵ primary: accept the selected seat, else enter a ruling if one is typed. */ - submitPrimary: () => void; -} - -export function useConvergeGavel(onResolve: ConvergeResolve): ConvergeGavelModel { - const [selectedSeatId, setSelectedSeatId] = useState(null); - const [note, setNote] = useState(''); - const [pending, setPending] = useState(null); - const [rejectConfirmOpen, setRejectConfirmOpen] = useState(false); - const [error, setError] = useState(null); - const busy = pending !== null; - - const dispatch = useCallback( - async (decision: CouncilConvergeDecision, options?: ConvergeResolveOptions) => { - setPending(decision); - setError(null); - try { - await onResolve(decision, options); - // On success the parent flips to `resolved` and unmounts the form, so `pending` - // is intentionally left set to keep controls disabled through the transition. - } catch (err) { - setPending(null); // re-enable on failure so the human can retry - setRejectConfirmOpen(false); // surface the inline error, not the dialog - setError( - err instanceof Error ? err.message : 'Could not record your verdict.', - ); - } - }, - [onResolve], - ); - - const trimmedNote = note.trim(); - const withNote = (): ConvergeResolveOptions | undefined => - trimmedNote.length > 0 ? { note: trimmedNote } : undefined; - - const accept = useCallback(() => { - if (selectedSeatId === null || busy) return; - void dispatch('accept', { seatId: selectedSeatId, ...withNote() }); - }, [busy, dispatch, selectedSeatId, trimmedNote]); - - const requestReject = useCallback(() => { - if (busy) return; - setRejectConfirmOpen(true); - }, [busy]); - - const closeRejectConfirm = useCallback(() => { - if (pending === 'reject') return; // don't dismiss mid-dispatch - setRejectConfirmOpen(false); - }, [pending]); - - const confirmReject = useCallback(() => { - if (busy) return; - void dispatch('reject', withNote()); - }, [busy, dispatch, trimmedNote]); - - const judge = useCallback(() => { - if (trimmedNote.length === 0 || busy) return; - void dispatch('judge', { note: trimmedNote }); - }, [busy, dispatch, trimmedNote]); - - const submitPrimary = useCallback(() => { - if (selectedSeatId !== null) accept(); - else if (trimmedNote.length > 0) judge(); - }, [accept, judge, selectedSeatId, trimmedNote]); - - return { - selectedSeatId, - select: setSelectedSeatId, - note, - setNote, - pending, - busy, - error, - canAccept: selectedSeatId !== null, - canJudge: trimmedNote.length > 0, - accept, - rejectConfirmOpen, - requestReject, - closeRejectConfirm, - confirmReject, - judge, - submitPrimary, - }; -} diff --git a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.stories.tsx b/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.stories.tsx deleted file mode 100644 index 328474c9..00000000 --- a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.stories.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import type { ConvergePosition } from '../council.types'; -import { ConvergeGavel } from './ConvergeGavel'; - -const POSITIONS: ConvergePosition[] = [ - { - seatId: 'proposer-opus', - role: 'proposer', - content: 'Feature-flag the store, dual-write with idempotency keys, then cut over.', - }, - { - seatId: 'proposer-sonnet', - role: 'proposer', - content: 'Big-bang migrate in a maintenance window — simpler, but no live rollback.', - }, - { - seatId: 'critic-opus', - role: 'critic', - content: 'Neither is safe without a rehearsed backfill + rollback. Prefer the flag.', - }, -]; - -const meta = { - title: 'Council/ConvergeGavel', - component: ConvergeGavel, - parameters: { layout: 'fullscreen' }, - args: { - positions: POSITIONS, - // The story dispatch resolves immediately; the real one routes through the Conductor. - onResolve: async () => {}, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Awaiting: Story = {}; - -export const NoPositionsYet: Story = { args: { positions: [] } }; - -export const Resolved: Story = { - args: { - resolved: true, - verdict: 'Human verdict — ACCEPT: adopted seat "proposer-opus" (proposer). Reason: safest rollback.', - }, -}; diff --git a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.test.tsx b/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.test.tsx deleted file mode 100644 index 1ed9b260..00000000 --- a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { composeStories } from '@storybook/react-vite'; -import { expect, test, vi } from 'vitest'; -import { render } from 'vitest-browser-react'; - -import type { ConvergePosition } from '../council.types'; -import { ConvergeGavel } from './ConvergeGavel'; -import * as stories from './ConvergeGavel.stories'; - -const { NoPositionsYet, Resolved } = composeStories(stories); - -const POSITIONS: ConvergePosition[] = [ - { seatId: 'proposer-opus', role: 'proposer', content: 'Flag the store.' }, - { seatId: 'critic-opus', role: 'critic', content: 'Rehearse rollback first.' }, -]; - -test('accept is disabled until a seat is selected, then resolves with that seat', async () => { - const onResolve = vi.fn(async () => {}); - const screen = render(); - - const accept = screen.getByRole('button', { name: /Accept selected/ }); - await expect.element(accept).toBeDisabled(); - - await screen.getByRole('radio', { name: "Adopt critic-opus's position" }).click(); - await expect.element(accept).toBeEnabled(); - await accept.click(); - - expect(onResolve).toHaveBeenCalledWith('accept', { seatId: 'critic-opus' }); -}); - -test('reject is confirmed through a dialog before it resolves', async () => { - const onResolve = vi.fn(async () => {}); - const screen = render(); - - // The toolbar button opens the guard dialog; nothing dispatches yet. - await screen.getByRole('button', { name: /Reject all/ }).click(); - const dialog = screen.getByRole('alertdialog'); - await expect.element(dialog).toBeInTheDocument(); - expect(onResolve).not.toHaveBeenCalled(); - - // Confirming rejects every position and closes the run. - await dialog.getByRole('button', { name: 'Reject and close' }).click(); - expect(onResolve).toHaveBeenCalledWith('reject', undefined); -}); - -test('judge is gated on a ruling, then resolves with the ruling note', async () => { - const onResolve = vi.fn(async () => {}); - const screen = render(); - - const judge = screen.getByRole('button', { name: /Enter my ruling/ }); - await expect.element(judge).toBeDisabled(); - - await screen.getByLabelText('Your ruling / reason').fill('Stage the cutover behind a flag.'); - await expect.element(judge).toBeEnabled(); - await judge.click(); - - expect(onResolve).toHaveBeenCalledWith('judge', { - note: 'Stage the cutover behind a flag.', - }); -}); - -test('a dispatch failure surfaces an inline error and re-enables the controls', async () => { - const onResolve = vi.fn(async () => { - throw new Error('no live session for this run'); - }); - const screen = render(); - - await screen.getByRole('button', { name: /Reject all/ }).click(); - await screen.getByRole('alertdialog').getByRole('button', { name: 'Reject and close' }).click(); - await expect.element(screen.getByRole('alert')).toHaveTextContent('no live session for this run'); - await expect.element(screen.getByRole('button', { name: /Reject all/ })).toBeEnabled(); -}); - -test('shows a waiting state before the seats have final positions', async () => { - const screen = render(); - await expect - .element(screen.getByText("Waiting for the seats' final positions…")) - .toBeInTheDocument(); -}); - -test('once resolved, shows the recorded verdict read-only with no actions', async () => { - const screen = render(); - await expect.element(screen.getByText('Verdict recorded')).toBeInTheDocument(); - await expect.element(screen.getByText(/adopted seat "proposer-opus"/)).toBeInTheDocument(); - await expect - .element(screen.getByRole('button', { name: /Accept selected/ })) - .not.toBeInTheDocument(); -}); diff --git a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.tsx b/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.tsx deleted file mode 100644 index 341f31b0..00000000 --- a/apps/web/src/components/council/ConvergeGavel/ConvergeGavel.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/** - * The human Converge gavel (issue #353) — P1's Converge stage is HUMAN-only, so this is - * the human's terminal authority over a council run (safety non-negotiable #7). The - * human weighs the seats' final positions side-by-side (disagreement is the product) and - * rules one of three ways: ACCEPT one seat's position, REJECT every position, or JUDGE - * with their own ruling. The verdict flows through the Conductor (the sole bus writer) - * onto the append-only transcript — never a direct store write from the surface (safety - * #1); this component only DISPATCHES the human's choice and never feeds text into a - * seat prompt. - * - * Mirrors the board's HITL dock conventions (plan approval / the verification gate): a - * bottom-docked panel, labelled controls, ⌘/Ctrl+↵ to submit the primary action, and - * sibling loading/empty/error affordances. - */ -import { - Badge, - Button, - CheckIcon, - CloseIcon, - ConfirmDialog, - ConfirmHint, - Markdown, - RefineIcon, - VerifiedIcon, -} from '@/components/ui'; - -import { SEAT_ROLE_TONE } from '../council-roles'; -import { useConvergeGavel } from './ConvergeGavel.hooks'; -import type { ConvergeGavelProps } from './ConvergeGavel.types'; - -const RULING_INPUT_ID = 'council-converge-ruling'; - -export function ConvergeGavel({ - positions, - onResolve, - resolved = false, - verdict, -}: ConvergeGavelProps) { - const gavel = useConvergeGavel(onResolve); - - // Resolved: the run is closed — show the recorded verdict read-only, no actions. - if (resolved) { - return ( -
-
- - - Verdict recorded - -
-

- {verdict ?? 'The human judge closed this council. The verdict is on the transcript.'} -

-
- ); - } - - return ( -
-
- -

- Converge — you're the judge -

- - Adopt a seat's position, write your own ruling, or reject them all. P1 Converge - is yours alone. - -
- - {positions.length === 0 ? ( -

- Waiting for the seats' final positions… -

- ) : ( - <> -
- - Adopt a position - -
- {positions.map((position) => { - const selected = gavel.selectedSeatId === position.seatId; - return ( - - ); - })} -
-
- -
- -