Bring the chat experience into the Vite/React-Router SPA on the exact typed Socket.IO /chat contract from @mosaicstack/types, replacing the /chat placeholder behind AuthGuard. Surfaces message:ack (with an accessible status), agent:start, streamed agent:text/agent:thinking, tool start/end status, agent:end with usage, session:info (thinking controls + routing decision), commands:manifest, command:result, command:approval (with a one-time approved-run affordance), system:reload (refreshing the rendered manifest), and error, and emits message/abort/set:thinking/command:execute/ command:approve with exact payloads. The gateway does not guarantee message:ack is the first event for a new conversation (session:info, and error on auth/session-creation failure, can both arrive first) — conversation-scoped events now adopt the conversation from whichever scoped event names it first while a send is pending, then filter everything else against that established conversation. A typed error stops streaming instead of leaving Stop stuck active; agent:end no longer appends an empty assistant turn when there is no text or thinking; and a second message can no longer be sent while a turn is streaming. Command approval is now integrity-checked end to end: only one command:approve request may be outstanding at a time (a concurrent request is ignored rather than overwriting the pending command/args), a stale or mismatched command:approval response cannot replace active approval state, and running an approved command clears its approval state immediately (via a ref, before React re-renders) so a double-click cannot replay command:execute. The `/chat` socket is now typed at a single boundary: apps/web/src/lib/ socket.ts narrows socket.io-client's untyped `io()` return value to `ChatSocket` (Socket<ServerToClientEvents, ClientToServerEvents>) once, at creation, via the one assertion the library's types force; every consumer (use-chat-connection.ts) then gets fully checked `on`/`emit` calls with no further casts. The shared contract types live in the new apps/web/src/lib/chat-contract.ts (replacing the old spa/chat/types.ts shim), which re-exports them via type-only imports resolved directly against packages/types/src (apps/web has no @mosaicstack/types package dependency, so this stays source-only and is erased at compile time — no package manifest or lockfile is touched). The two recorded-event test suites now drive a shared, typed fake socket (spa/chat/test-support/fake-chat-socket.ts) instead of an untyped `(event: string, payload: unknown)` harness, so a wrong event name or malformed payload fails to compile.
44 lines
1.2 KiB
TypeScript
44 lines
1.2 KiB
TypeScript
import type { ReactElement } from 'react';
|
|
import type { SessionInfoPayload } from '@/lib/chat-contract';
|
|
|
|
interface SessionPanelProps {
|
|
sessionInfo: SessionInfoPayload | null;
|
|
onSetThinking: (level: string) => void;
|
|
}
|
|
|
|
export function SessionPanel({
|
|
sessionInfo,
|
|
onSetThinking,
|
|
}: SessionPanelProps): ReactElement | null {
|
|
if (!sessionInfo) return null;
|
|
|
|
return (
|
|
<section
|
|
aria-label="Session info"
|
|
className="flex flex-wrap items-center gap-3 border-b px-4 py-2 text-xs"
|
|
>
|
|
<span>{sessionInfo.provider}</span>
|
|
<span>{sessionInfo.modelId}</span>
|
|
<label className="flex items-center gap-2">
|
|
<span>Thinking level</span>
|
|
<select
|
|
aria-label="Thinking level"
|
|
value={sessionInfo.thinkingLevel}
|
|
onChange={(event) => onSetThinking(event.target.value)}
|
|
>
|
|
{sessionInfo.availableThinkingLevels.map((level) => (
|
|
<option key={level} value={level}>
|
|
{level}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
{sessionInfo.routingDecision ? (
|
|
<span title={sessionInfo.routingDecision.ruleName}>
|
|
{sessionInfo.routingDecision.reason}
|
|
</span>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|