fix(web): harden typed SPA chat lifecycle

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
shaggy (mosaic-dev box)
2026-08-10 00:24:20 -05:00
co-authored by Claude Haiku 4.5
parent b2e005f2b4
commit caebf9ef70
20 changed files with 1778 additions and 125 deletions
+55 -18
View File
@@ -1,11 +1,16 @@
import { useState, type ReactElement } from 'react';
import type { PendingApproval } from './use-chat-connection';
import { asString } from './runtime-guards';
import type {
CommandManifest,
SlashCommandApprovalResultPayload,
SlashCommandResultPayload,
} from '@/lib/chat-contract';
/** Stable client copy shown for a failed command — never the raw server
* detail, which could leak internal error text to the user. */
const COMMAND_FAILURE_COPY = 'Command failed.';
interface CommandsPanelProps {
manifest: CommandManifest | null;
results: SlashCommandResultPayload[];
@@ -30,19 +35,39 @@ export function CommandsPanel({
const [command, setCommand] = useState('');
const [args, setArgs] = useState('');
// Defense-in-depth: the reducer already normalizes success/approvalId
// before storing `approval`, but a matching command string alone must
// never be trusted here either — require the literal boolean `true` and a
// non-empty string approvalId, not merely truthy values.
const canRunApproved =
!!approval?.success &&
!!approval.approvalId &&
approval?.success === true &&
typeof approval.approvalId === 'string' &&
approval.approvalId.length > 0 &&
!!pendingApproval &&
pendingApproval.command === approval.command;
// A manifest arrives from the server as untyped JSON at runtime — guard
// both collections before mapping so a malformed manifest cannot throw.
const commands = Array.isArray(manifest?.commands) ? manifest.commands : [];
const skills = Array.isArray(manifest?.skills) ? manifest.skills : [];
return (
<section aria-label="Commands" className="flex flex-col gap-2 border-b px-4 py-3 text-xs">
{manifest && manifest.commands.length > 0 ? (
{commands.length > 0 ? (
<ul aria-label="Available commands" className="flex flex-col gap-1">
{manifest.commands.map((cmd) => (
<li key={cmd.name}>
<strong>/{cmd.name}</strong> {cmd.description}
{commands.map((cmd, index) => (
<li key={asString(cmd?.name) || `cmd-${index}`}>
<strong>/{asString(cmd?.name)}</strong> {asString(cmd?.description)}
</li>
))}
</ul>
) : null}
{skills.length > 0 ? (
<ul aria-label="Available skills" className="flex flex-col gap-1">
{skills.map((skill, index) => (
<li key={asString(skill?.name) || `skill-${index}`}>
<strong>/skill:{asString(skill?.name)}</strong> {asString(skill?.description)}
</li>
))}
</ul>
@@ -79,16 +104,24 @@ export function CommandsPanel({
{approval ? (
<div role={approval.success ? 'status' : 'alert'} className="flex items-center gap-2">
<span>
{approval.success
? `Approved: /${approval.command}`
: `Approval denied: /${approval.command}`}
{approval.message ? `${approval.message}` : ''}
</span>
{canRunApproved ? (
<button type="button" onClick={onRunApproved}>
Run approved command
</button>
{/* Stable client copy only — never the server-controlled
approval.message or echoed approval.command as the primary
confirmation. The frozen local pendingApproval below (not this
line) is the sole authoritative statement of what will run. */}
<span>{approval.success ? 'Approved.' : 'Denied.'}</span>
{canRunApproved && pendingApproval ? (
<>
{/* Authoritative frozen local command+args — what the click below
will actually emit. The server's `approval` above is display-only
and must never be trusted to represent the executed payload. */}
<span>
Will run: /{pendingApproval.command}{' '}
{pendingApproval.args ? pendingApproval.args : '(no args)'}
</span>
<button type="button" onClick={onRunApproved}>
Run approved command
</button>
</>
) : null}
</div>
) : null}
@@ -97,8 +130,12 @@ export function CommandsPanel({
<ul aria-label="Command results" className="flex flex-col gap-1">
{results.map((result, index) => (
<li key={`${result.command}-${index}`} role={result.success ? 'status' : 'alert'}>
/{result.command}: {result.success ? 'success' : 'failed'}
{result.message ? `${result.message}` : ''}
/{asString(result.command)}: {result.success ? 'success' : 'failed'}
{result.success
? typeof result.message === 'string' && result.message
? `${result.message}`
: ''
: `${COMMAND_FAILURE_COPY}`}
</li>
))}
</ul>