69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import type { ReactElement } from 'react';
|
|
import type { SessionInfoPayload } from '@/lib/chat-contract';
|
|
import { MAX_MANIFEST_ITEMS } from './limits';
|
|
import { asString, asStringArray } from './runtime-guards';
|
|
|
|
interface SessionPanelProps {
|
|
sessionInfo: SessionInfoPayload | null;
|
|
onSetThinking: (level: string) => void;
|
|
}
|
|
|
|
const THINKING_LEVEL_UNAVAILABLE = '';
|
|
|
|
export function SessionPanel({
|
|
sessionInfo,
|
|
onSetThinking,
|
|
}: SessionPanelProps): ReactElement | null {
|
|
if (!sessionInfo) return null;
|
|
|
|
// The reducer already caps this before storing it, but the render site
|
|
// defends independently — a hostile payload must never be able to force
|
|
// this <select> to lay out an unbounded number of options.
|
|
const availableThinkingLevels = asStringArray(sessionInfo.availableThinkingLevels).slice(
|
|
0,
|
|
MAX_MANIFEST_ITEMS,
|
|
);
|
|
const hasThinkingLevels = availableThinkingLevels.length > 0;
|
|
|
|
return (
|
|
<section
|
|
aria-label="Session info"
|
|
className="flex flex-wrap items-center gap-3 border-b px-4 py-2 text-xs"
|
|
>
|
|
<span>{asString(sessionInfo.provider, 'unknown')}</span>
|
|
<span>{asString(sessionInfo.modelId, 'unknown')}</span>
|
|
<label className="flex items-center gap-2">
|
|
<span>Thinking level</span>
|
|
<select
|
|
aria-label="Thinking level"
|
|
value={
|
|
hasThinkingLevels ? asString(sessionInfo.thinkingLevel) : THINKING_LEVEL_UNAVAILABLE
|
|
}
|
|
onChange={(event) => {
|
|
// The placeholder option is not a real, settable level — a
|
|
// malformed availableThinkingLevels list must never let the
|
|
// client emit set:thinking for it.
|
|
if (!hasThinkingLevels) return;
|
|
onSetThinking(event.target.value);
|
|
}}
|
|
>
|
|
{hasThinkingLevels ? (
|
|
availableThinkingLevels.map((level) => (
|
|
<option key={level} value={level}>
|
|
{level}
|
|
</option>
|
|
))
|
|
) : (
|
|
<option value={THINKING_LEVEL_UNAVAILABLE}>Thinking level unavailable</option>
|
|
)}
|
|
</select>
|
|
</label>
|
|
{sessionInfo.routingDecision ? (
|
|
<span title={asString(sessionInfo.routingDecision.ruleName)}>
|
|
{asString(sessionInfo.routingDecision.reason)}
|
|
</span>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|