+ {/* header */}
+
+
+
+
+
+
{name}
+
+
+ {busy ? t("sagi.chat.typing") : toolLabel}
+
+
+ {chat && (
+
onOpenWorkspace(chat.laneId, chat.runId)}
+ >
+
+
+ )}
+
remember(null)}
+ disabled={!chat || starting}
+ >
+
+
+
+
+
+
+
+ {/* conversation */}
+
+ {empty && (
+
+
+ 🐾
+
+
+ {t("sagi.chat.emptyTitle", { name })}
+
+
{t("sagi.chat.emptyHint", { tool: toolLabel })}
+
+ )}
+ {lines.map((l) =>
+ l.role === "tools" ? (
+
+
+
+ {toolSummary(l.names)}
+
+
+ ) : l.role === "user" ? (
+
+ ) : (
+
+ )
+ )}
+ {pending && (
+
+ )}
+ {busy && !menu && (
+
+ )}
+ {starting && (
+
+
+ {t("sagi.chat.starting", { tool: toolLabel })}
+
+ )}
+ {chat && !sessionId && !starting && (
+
{t("sagi.chat.notStarted")}
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* prompt, or the question the CLI asks */}
+
+ {menu && chat ? (
+
+
{
+ if (chooseRunOption(chat.runId, menu, i)) setMenu(null);
+ else setError(t("sagi.chat.notSent"));
+ }}
+ onAnswer={(text) => {
+ if (answerRunMenu(chat.runId, menu, text)) setMenu(null);
+ else setError(t("sagi.chat.notSent"));
+ }}
+ onCancel={() => {
+ if (interruptRun(chat.runId)) setMenu(null);
+ else setError(t("sagi.chat.notSent"));
+ }}
+ />
+
+ ) : (
+
+ )}
+
+
+ {/* The chat's pane, attached off-screen: the only way a follow-up or a
+ menu answer reaches it. A real size, so tmux does not shrink the pane. */}
+ {chat && (
+
+ {
+ writePetChat(null);
+ setChat(null);
+ }}
+ />
+
+ )}
+
+ );
+}
+
+function IconButton({
+ label,
+ onClick,
+ disabled,
+ children,
+}: {
+ label: string;
+ onClick: () => void;
+ disabled?: boolean;
+ children: React.ReactNode;
+}) {
+ return (
+