{/* Pi queues mid-run sends (follow-up/steer), so Send stays available
while text is typed even during a run; Stop shows when empty. */}
!s.thread.isRunning || !s.composer.isEmpty}>
s.thread.isRunning && s.composer.isEmpty}>
);
};
const selectedModelKey = (provider?: string, modelId?: string) =>
provider && modelId ? modelKey(provider, modelId) : undefined;
/** Model + thinking-level picker seeded from the server handshake and wired to
* Pi's per-thread `session.setModel`/`session.setThinkingLevel`. Per-model
* thinking levels come from the handshake; Pi clamps unsupported levels and the
* effective config is reflected by the next snapshot/event. */
const ComposerModelSelector: FC = () => {
const handshake = usePiHandshake();
const { metadata, setModel, setThinkingLevel, status } = usePiRuntimeExtras();
const selected =
selectedModelKey(metadata.config?.provider, metadata.config?.modelId) ??
handshake?.selectedModelId;
const thinkingLevel = metadata.config?.thinkingLevel;
if (!handshake || handshake.models.length === 0) return null;
return (
{
if (status === "running") return;
const model = handshake.models.find((m) => m.id === value);
if (!model) return;
void setModel({ provider: model.provider, modelId: model.modelId });
}}
effort={typeof thinkingLevel === "string" ? thinkingLevel : "medium"}
onEffortChange={(effort) => {
if (status === "running") return;
void setThinkingLevel(effort as PiThinkingLevel);
}}
>
{/* Custom slider instead of ModelSelector.Effort: Pi's six thinking
levels overflow the horizontal segmented layout. */}
);
};
/** Context-window usage as a ring beside the send button. Driven by Pi's
* `context_usage` events (`usePiThreadState`); hidden until the first run
* reports a window. */
const ComposerContextRing: FC = () => {
const usage = usePiThreadState((s) => s.contextUsage);
if (!usage || !usage.contextWindow) return null;
return (
);
};
const MessageError: FC = () => {
return (
);
};
const AssistantMessage: FC = () => {
// reserves space for action bar and compensates with `-mb` for consistent msg spacing
// keeps hovered action bar from shifting layout (autohide doesn't support absolute positioning well)
// for pt-[n] use -mb-[n + 6] & min-h-[n + 6] to preserve compensation
const ACTION_BAR_PT = "pt-1.5";
const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`;
return (
{({ part, children }) => {
switch (part.type) {
case "group-chainOfThought":
return
{children}
;
case "group-reasoning": {
const running = part.status.type === "running";
return (
{children}
);
}
case "group-tool":
if (part.indices.length === 1) return <>{children}>;
return (
{children}
);
case "text":
return ;
case "reasoning":
return ;
case "tool-call":
return part.toolUI ?? ;
case "indicator":
return (
{"●"}
);
default:
return null;
}
}}
);
};
const AssistantActionBar: FC = () => {
return (
s.message.isCopied}>
!s.message.isCopied}>
Export as Markdown
);
};
const UserMessage: FC = () => {
return (