fix: stabilize PicoClaw session switching

- Wait for gateway session release before reconnecting to a new or selected session
- Refresh runtime status before deciding whether a selected history session can reconnect
- Render structured thought messages from gateway payload metadata
- Prevent PicoClaw history rows from overflowing horizontally
- Add localized labels for the verifying install stage
This commit is contained in:
wenjie
2026-05-18 17:26:00 +08:00
parent 3f0621e7ed
commit a960417739
31 changed files with 66 additions and 31 deletions

View File

@@ -109,14 +109,14 @@ export function sendStopMessage() {
return picoclawGateway.sendStopMessage();
}
export function closeGateway() {
export async function closeGateway() {
const activeSessionId = picoclawGateway.getSessionId();
picoclawGateway.close();
if (!activeSessionId) {
return;
}
void releaseRuntimeSession(activeSessionId).catch(() => undefined);
await releaseRuntimeSession(activeSessionId).catch(() => undefined);
}
export function onGatewayConnectionState(listener: (state: GatewayTransportState) => void) {

View File

@@ -541,6 +541,7 @@ const ca = {
preparing: 'Preparant',
downloading: "S'està baixant",
extracting: 'Extracció',
verifying: 'Verificant',
installing: 'Instal·lant',
installed: 'Instal·lat',
install_timeout: 'Temps esgotat',

View File

@@ -546,6 +546,7 @@ const cz = {
preparing: 'Příprava',
downloading: 'Stahování',
extracting: 'Extrakce',
verifying: 'Ověřování',
installing: 'Instalace probíhá',
installed: 'Instalováno',
install_timeout: 'Vypršel časový limit',

View File

@@ -545,6 +545,7 @@ const da = {
preparing: 'Forberedelse',
downloading: 'Downloader',
extracting: 'Udpakning',
verifying: 'Bekræfter',
installing: 'Installerer',
installed: 'Installeret',
install_timeout: 'Timeout',

View File

@@ -551,6 +551,7 @@ const de = {
preparing: 'Vorbereiten',
downloading: 'Wird heruntergeladen',
extracting: 'Extrahieren',
verifying: 'Überprüfen',
installing: 'Installiere',
installed: 'Installiert',
install_timeout: 'Zeitüberschreitung',

View File

@@ -543,6 +543,7 @@ const en = {
preparing: 'Preparing',
downloading: 'Downloading',
extracting: 'Extracting',
verifying: 'Verifying',
installing: 'Installing',
installed: 'Installed',
install_timeout: 'Timed Out',

View File

@@ -550,6 +550,7 @@ const es = {
preparing: 'Preparando',
downloading: 'Descargando',
extracting: 'Extrayendo',
verifying: 'Verificando',
installing: 'Instalando',
installed: 'Instalado',
install_timeout: 'Tiempo de espera agotado',

View File

@@ -551,6 +551,7 @@ const fr = {
preparing: 'Préparation',
downloading: 'Téléchargement',
extracting: 'Extraction',
verifying: 'Vérification',
installing: 'Installation',
installed: 'Installé',
install_timeout: 'Délai expiré',

View File

@@ -549,6 +549,7 @@ const hu = {
preparing: 'Felkészülés',
downloading: 'Letöltés',
extracting: 'Kibontás',
verifying: 'Ellenőrzés',
installing: 'Telepítés folyamatban',
installed: 'Telepítve',
install_timeout: 'Időtúllépés',

View File

@@ -545,6 +545,7 @@ const id = {
preparing: 'Mempersiapkan',
downloading: 'Mengunduh',
extracting: 'Mengekstraksi',
verifying: 'Memverifikasi',
installing: 'Memasangkan',
installed: 'Terpasang',
install_timeout: 'Waktu Habis',

View File

@@ -549,6 +549,7 @@ const it = {
preparing: 'Preparazione',
downloading: 'Download in corso',
extracting: 'Estrazione',
verifying: 'Verifica in corso',
installing: 'Installazione in corso',
installed: 'Installato',
install_timeout: 'Timeout',

View File

@@ -549,6 +549,7 @@ const ja = {
preparing: '準備中',
downloading: 'ダウンロード中',
extracting: '展開中',
verifying: '検証中',
installing: 'インストール中',
installed: 'インストール完了',
install_timeout: 'タイムアウト',

View File

@@ -539,6 +539,7 @@ const ko = {
preparing: '준비 중',
downloading: '다운로드 중',
extracting: '압축 해제 중',
verifying: '검증 중',
installing: '설치 중',
installed: '설치 완료',
install_timeout: '시간 초과',

View File

@@ -545,6 +545,7 @@ const nb = {
preparing: 'Forbereder',
downloading: 'Laster ned',
extracting: 'Pakker ut',
verifying: 'Verifiserer',
installing: 'Installerer',
installed: 'Installert',
install_timeout: 'Tidsavbrudd',

View File

@@ -549,6 +549,7 @@ const nl = {
preparing: 'Voorbereiden',
downloading: 'Downloaden',
extracting: 'Uitpakken',
verifying: 'Verifiëren',
installing: 'Installeren',
installed: 'Geïnstalleerd',
install_timeout: 'Time-out',

View File

@@ -548,6 +548,7 @@ const pl = {
preparing: 'Przygotowanie',
downloading: 'Pobieranie',
extracting: 'Wypakowywanie',
verifying: 'Weryfikowanie',
installing: 'Instalowanie',
installed: 'Zainstalowano',
install_timeout: 'Upłynął limit czasu',

View File

@@ -546,6 +546,7 @@ const pt_br = {
preparing: 'Preparando',
downloading: 'Baixando',
extracting: 'Extraindo',
verifying: 'Verificando',
installing: 'Instalando',
installed: 'Instalado',
install_timeout: 'Tempo limite esgotado',

View File

@@ -547,6 +547,7 @@ const ru = {
preparing: 'Подготовка',
downloading: 'Загрузка',
extracting: 'Распаковка',
verifying: 'Проверка',
installing: 'Установка',
installed: 'Установлено',
install_timeout: 'Время ожидания истекло',

View File

@@ -541,6 +541,7 @@ const se = {
preparing: 'Förbereder',
downloading: 'Laddar ner',
extracting: 'Packar upp',
verifying: 'Verifierar',
installing: 'Installerar',
installed: 'Installerad',
install_timeout: 'Timeout',

View File

@@ -538,6 +538,7 @@ const th = {
preparing: 'กำลังเตรียมการ',
downloading: 'กำลังดาวน์โหลด',
extracting: 'กำลังแตกไฟล์',
verifying: 'กำลังตรวจสอบ',
installing: 'กำลังติดตั้ง',
installed: 'ติดตั้งแล้ว',
install_timeout: 'หมดเวลา',

View File

@@ -545,6 +545,7 @@ const tr = {
preparing: 'Hazırlanıyor',
downloading: 'İndiriliyor',
extracting: ıkarılıyor',
verifying: 'Doğrulanıyor',
installing: 'Yükleniyor',
installed: 'Yüklendi',
install_timeout: 'Zaman Aşımı',

View File

@@ -546,6 +546,7 @@ const uk = {
preparing: 'Підготовка',
downloading: 'Завантаження',
extracting: 'Розпакування',
verifying: 'Перевірка',
installing: 'Встановлення',
installed: 'Встановлено',
install_timeout: 'Час очікування минув',

View File

@@ -544,6 +544,7 @@ const vi = {
preparing: 'Đang chuẩn bị',
downloading: 'Đang tải xuống',
extracting: 'Đang giải nén',
verifying: 'Đang xác minh',
installing: 'Đang cài đặt',
installed: 'Đã cài đặt',
install_timeout: 'Đã hết thời gian',

View File

@@ -533,6 +533,7 @@ const zh = {
preparing: '准备中',
downloading: '下载中',
extracting: '解压中',
verifying: '验证中',
installing: '安装中',
installed: '已安装',
install_timeout: '已超时',

View File

@@ -533,6 +533,7 @@ const zh_tw = {
preparing: '準備中',
downloading: '下載中',
extracting: '解壓縮中',
verifying: '驗證中',
installing: '安裝中',
installed: '已安裝',
install_timeout: '超時',

View File

@@ -10,9 +10,6 @@ type MessageProps = {
message: PicoclawChatMessage;
};
const hiddenAssistantMessagePrefix = '🔧 `message`';
const nullToolFeedbackPattern = /^🔧\s*`[^`]+`\s*```(?:[\w-]+)?\s*null\s*```$/s;
function extractDisplayText(value: unknown): string {
if (typeof value === 'string') {
const normalized = normalizeLiteralEmptyText(value);
@@ -77,15 +74,17 @@ function normalizeLiteralEmptyText(value: string): string {
return trimmed;
}
function shouldHideAssistantMessage(value?: string): boolean {
const normalized = normalizeLiteralEmptyText(value || '');
if (!normalized) {
function isThoughtMessage(raw: unknown): boolean {
if (!raw || typeof raw !== 'object') {
return false;
}
return (
normalized.startsWith(hiddenAssistantMessagePrefix) || nullToolFeedbackPattern.test(normalized)
);
const payload = (raw as Record<string, unknown>).payload;
if (!payload || typeof payload !== 'object') {
return false;
}
return (payload as Record<string, unknown>).kind === 'thought';
}
export const Message = ({ message }: MessageProps) => {
@@ -153,8 +152,17 @@ export const Message = ({ message }: MessageProps) => {
);
}
if (message.kind === 'assistant' && shouldHideAssistantMessage(message.text)) {
return null;
if (message.kind === 'assistant' && isThoughtMessage(message.raw)) {
const thoughtText = extractDisplayText(message.text);
if (!thoughtText) {
return null;
}
return (
<div className="border-white/5 rounded-xl border bg-white/[0.02] px-3 py-2 text-xs text-neutral-400">
<MarkdownContent content={thoughtText} className="break-words" />
</div>
);
}
const isUser = message.kind === 'user';

View File

@@ -145,7 +145,7 @@ export function createPicoclawSidebarActions(options: PicoclawSidebarActionOptio
]);
if (isRuntimeReady) {
closeGateway();
void closeGateway();
setTakeover({
active: false,
sessionId: picoclawGateway.getSessionId(),
@@ -194,7 +194,7 @@ export function createPicoclawSidebarActions(options: PicoclawSidebarActionOptio
]);
if (isRuntimeReady) {
closeGateway();
void closeGateway();
setTakeover({
active: false,
sessionId: picoclawGateway.getSessionId(),

View File

@@ -228,7 +228,7 @@ export function usePicoclawSidebarLifecycle({
return () => {
cancelled = true;
closeGateway();
void closeGateway();
setTakeover((current) => ({
...current,
active: false,

View File

@@ -61,8 +61,8 @@ export const SidebarHistory = ({
}
return (
<div className="picoclaw-sidebar-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto px-3 py-3">
<div className="space-y-2">
<div className="picoclaw-sidebar-scrollbar box-border flex min-h-0 w-full flex-1 flex-col overflow-x-hidden overflow-y-auto px-3 py-3">
<div className="box-border w-full max-w-full space-y-2">
{sessions.map((session) => {
const isActive = session.id === activeSessionId;
return (
@@ -86,7 +86,7 @@ export const SidebarHistory = ({
}
}}
className={[
'group flex w-full items-start gap-3 rounded-2xl border px-3 py-3 text-left transition-colors',
'group box-border flex w-full items-start gap-3 rounded-2xl border px-3 py-3 text-left transition-colors',
isSwitching ? 'pointer-events-none opacity-60' : '',
isActive
? 'border-sky-400/30 bg-sky-400/10'
@@ -98,7 +98,7 @@ export const SidebarHistory = ({
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-neutral-200">
{session.title}
</div>

View File

@@ -32,7 +32,6 @@ type TakeoverSetter = Dispatch<SetStateAction<PicoclawTakeoverState>>;
type PicoclawSidebarSessionActionOptions = {
t: TFunction;
runtimeStatus: PicoclawRuntimeStatus | null;
transportState: PicoclawTransportState;
runState: PicoclawRunState;
config: PicoclawConfigState;
@@ -59,7 +58,6 @@ type PicoclawSidebarSessionActionOptions = {
export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSessionActionOptions) {
const {
t,
runtimeStatus,
transportState,
runState,
config,
@@ -142,7 +140,7 @@ export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSess
return;
}
closeGateway();
const closePromise = closeGateway();
setIsHistoryOpen(false);
setIsModelConfigOpen(false);
setMessages([]);
@@ -159,6 +157,7 @@ export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSess
setActiveSessionId(nextSessionId);
setIsFreshConversation(true);
await closePromise;
const nextRuntimeStatus = await refreshState();
if (!canConnectGateway(nextRuntimeStatus ?? null)) {
return;
@@ -225,7 +224,7 @@ export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSess
})
);
closeGateway();
const closePromise = closeGateway();
setMessages(loadedMessages);
setActiveSessionId(sessionId);
setIsFreshConversation(false);
@@ -238,6 +237,9 @@ export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSess
setOverlay(HIDDEN_OVERLAY);
setTransportState('disconnected');
setRunState('idle');
await closePromise;
const nextRuntimeStatus = await refreshState();
setRuntimeStatus((current) =>
current
? {
@@ -247,12 +249,14 @@ export function createPicoclawSidebarSessionActions(options: PicoclawSidebarSess
: current
);
if (canConnectGateway(runtimeStatus ?? null)) {
try {
await connectGateway(sessionId);
} catch {
// handled by gateway events
}
if (!canConnectGateway(nextRuntimeStatus ?? null)) {
return;
}
try {
await connectGateway(sessionId);
} catch {
// handled by gateway events
}
} catch (error) {
const errorMessage =

View File

@@ -107,7 +107,6 @@ export const useSidebar = () => {
refreshStateRef.current = actions.refreshState;
const sessionActions = createPicoclawSidebarSessionActions({
t,
runtimeStatus,
transportState,
runState,
config,