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
+22 -5
View File
@@ -2,10 +2,25 @@ import type { ReactElement } from 'react';
import { CommandsPanel } from '@/spa/chat/commands-panel';
import { Composer } from '@/spa/chat/composer';
import { MessageTranscript } from '@/spa/chat/message-transcript';
import { asFiniteNumberOrNull, asString } from '@/spa/chat/runtime-guards';
import { SessionPanel } from '@/spa/chat/session-panel';
import { ToolCallList } from '@/spa/chat/tool-call-list';
import { useChatConnection } from '@/spa/chat/use-chat-connection';
/** Renders a real value normally, but an honest "unavailable" label instead
* of a fabricated `0` for a missing/malformed count — a real `0 tokens` and
* an unknown token count must never look the same. */
function formatTokens(value: unknown): string {
const tokens = asFiniteNumberOrNull(value);
return tokens === null ? 'tokens unavailable' : `${tokens} tokens`;
}
/** Same honesty guarantee as `formatTokens`, for cost. */
function formatCost(value: unknown): string {
const cost = asFiniteNumberOrNull(value);
return cost === null ? 'cost unavailable' : `$${cost.toFixed(4)}`;
}
export function ChatPage(): ReactElement {
const { state, actions } = useChatConnection();
const hasConversation = state.conversationId !== null;
@@ -18,19 +33,20 @@ export function ChatPage(): ReactElement {
{state.systemReload ? (
<div role="status" className="border-b px-4 py-2 text-sm">
{state.systemReload.message}
{asString(state.systemReload.message)}
</div>
) : null}
{state.error ? (
<div role="alert" className="border-b px-4 py-2 text-sm">
{state.error}
{asString(state.error)}
</div>
) : null}
{state.ack ? (
<div role="status" className="border-b px-4 py-1 text-xs opacity-70">
Message accepted · conversation {state.ack.conversationId} · id {state.ack.messageId}
Message accepted · conversation {asString(state.ack.conversationId, 'unknown')} · id{' '}
{asString(state.ack.messageId, 'unknown')}
</div>
) : null}
@@ -48,8 +64,8 @@ export function ChatPage(): ReactElement {
{state.usage ? (
<div aria-label="Usage" className="px-4 pb-2 text-xs opacity-80">
{state.usage.tokens.total} tokens · ${state.usage.cost.toFixed(4)} ·{' '}
{state.usage.provider}/{state.usage.modelId}
{formatTokens(state.usage.tokens?.total)} · {formatCost(state.usage.cost)} ·{' '}
{asString(state.usage.provider, 'unknown')}/{asString(state.usage.modelId, 'unknown')}
</div>
) : null}
@@ -68,6 +84,7 @@ export function ChatPage(): ReactElement {
onSend={actions.sendMessage}
onStop={actions.abort}
streaming={state.streaming}
sending={state.sending}
hasConversation={hasConversation}
/>
</div>