mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
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:
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -545,6 +545,7 @@ const da = {
|
||||
preparing: 'Forberedelse',
|
||||
downloading: 'Downloader',
|
||||
extracting: 'Udpakning',
|
||||
verifying: 'Bekræfter',
|
||||
installing: 'Installerer',
|
||||
installed: 'Installeret',
|
||||
install_timeout: 'Timeout',
|
||||
|
||||
@@ -551,6 +551,7 @@ const de = {
|
||||
preparing: 'Vorbereiten',
|
||||
downloading: 'Wird heruntergeladen',
|
||||
extracting: 'Extrahieren',
|
||||
verifying: 'Überprüfen',
|
||||
installing: 'Installiere',
|
||||
installed: 'Installiert',
|
||||
install_timeout: 'Zeitüberschreitung',
|
||||
|
||||
@@ -543,6 +543,7 @@ const en = {
|
||||
preparing: 'Preparing',
|
||||
downloading: 'Downloading',
|
||||
extracting: 'Extracting',
|
||||
verifying: 'Verifying',
|
||||
installing: 'Installing',
|
||||
installed: 'Installed',
|
||||
install_timeout: 'Timed Out',
|
||||
|
||||
@@ -550,6 +550,7 @@ const es = {
|
||||
preparing: 'Preparando',
|
||||
downloading: 'Descargando',
|
||||
extracting: 'Extrayendo',
|
||||
verifying: 'Verificando',
|
||||
installing: 'Instalando',
|
||||
installed: 'Instalado',
|
||||
install_timeout: 'Tiempo de espera agotado',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -545,6 +545,7 @@ const id = {
|
||||
preparing: 'Mempersiapkan',
|
||||
downloading: 'Mengunduh',
|
||||
extracting: 'Mengekstraksi',
|
||||
verifying: 'Memverifikasi',
|
||||
installing: 'Memasangkan',
|
||||
installed: 'Terpasang',
|
||||
install_timeout: 'Waktu Habis',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -549,6 +549,7 @@ const ja = {
|
||||
preparing: '準備中',
|
||||
downloading: 'ダウンロード中',
|
||||
extracting: '展開中',
|
||||
verifying: '検証中',
|
||||
installing: 'インストール中',
|
||||
installed: 'インストール完了',
|
||||
install_timeout: 'タイムアウト',
|
||||
|
||||
@@ -539,6 +539,7 @@ const ko = {
|
||||
preparing: '준비 중',
|
||||
downloading: '다운로드 중',
|
||||
extracting: '압축 해제 중',
|
||||
verifying: '검증 중',
|
||||
installing: '설치 중',
|
||||
installed: '설치 완료',
|
||||
install_timeout: '시간 초과',
|
||||
|
||||
@@ -545,6 +545,7 @@ const nb = {
|
||||
preparing: 'Forbereder',
|
||||
downloading: 'Laster ned',
|
||||
extracting: 'Pakker ut',
|
||||
verifying: 'Verifiserer',
|
||||
installing: 'Installerer',
|
||||
installed: 'Installert',
|
||||
install_timeout: 'Tidsavbrudd',
|
||||
|
||||
@@ -549,6 +549,7 @@ const nl = {
|
||||
preparing: 'Voorbereiden',
|
||||
downloading: 'Downloaden',
|
||||
extracting: 'Uitpakken',
|
||||
verifying: 'Verifiëren',
|
||||
installing: 'Installeren',
|
||||
installed: 'Geïnstalleerd',
|
||||
install_timeout: 'Time-out',
|
||||
|
||||
@@ -548,6 +548,7 @@ const pl = {
|
||||
preparing: 'Przygotowanie',
|
||||
downloading: 'Pobieranie',
|
||||
extracting: 'Wypakowywanie',
|
||||
verifying: 'Weryfikowanie',
|
||||
installing: 'Instalowanie',
|
||||
installed: 'Zainstalowano',
|
||||
install_timeout: 'Upłynął limit czasu',
|
||||
|
||||
@@ -546,6 +546,7 @@ const pt_br = {
|
||||
preparing: 'Preparando',
|
||||
downloading: 'Baixando',
|
||||
extracting: 'Extraindo',
|
||||
verifying: 'Verificando',
|
||||
installing: 'Instalando',
|
||||
installed: 'Instalado',
|
||||
install_timeout: 'Tempo limite esgotado',
|
||||
|
||||
@@ -547,6 +547,7 @@ const ru = {
|
||||
preparing: 'Подготовка',
|
||||
downloading: 'Загрузка',
|
||||
extracting: 'Распаковка',
|
||||
verifying: 'Проверка',
|
||||
installing: 'Установка',
|
||||
installed: 'Установлено',
|
||||
install_timeout: 'Время ожидания истекло',
|
||||
|
||||
@@ -541,6 +541,7 @@ const se = {
|
||||
preparing: 'Förbereder',
|
||||
downloading: 'Laddar ner',
|
||||
extracting: 'Packar upp',
|
||||
verifying: 'Verifierar',
|
||||
installing: 'Installerar',
|
||||
installed: 'Installerad',
|
||||
install_timeout: 'Timeout',
|
||||
|
||||
@@ -538,6 +538,7 @@ const th = {
|
||||
preparing: 'กำลังเตรียมการ',
|
||||
downloading: 'กำลังดาวน์โหลด',
|
||||
extracting: 'กำลังแตกไฟล์',
|
||||
verifying: 'กำลังตรวจสอบ',
|
||||
installing: 'กำลังติดตั้ง',
|
||||
installed: 'ติดตั้งแล้ว',
|
||||
install_timeout: 'หมดเวลา',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -546,6 +546,7 @@ const uk = {
|
||||
preparing: 'Підготовка',
|
||||
downloading: 'Завантаження',
|
||||
extracting: 'Розпакування',
|
||||
verifying: 'Перевірка',
|
||||
installing: 'Встановлення',
|
||||
installed: 'Встановлено',
|
||||
install_timeout: 'Час очікування минув',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -533,6 +533,7 @@ const zh = {
|
||||
preparing: '准备中',
|
||||
downloading: '下载中',
|
||||
extracting: '解压中',
|
||||
verifying: '验证中',
|
||||
installing: '安装中',
|
||||
installed: '已安装',
|
||||
install_timeout: '已超时',
|
||||
|
||||
@@ -533,6 +533,7 @@ const zh_tw = {
|
||||
preparing: '準備中',
|
||||
downloading: '下載中',
|
||||
extracting: '解壓縮中',
|
||||
verifying: '驗證中',
|
||||
installing: '安裝中',
|
||||
installed: '已安裝',
|
||||
install_timeout: '超時',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -228,7 +228,7 @@ export function usePicoclawSidebarLifecycle({
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
closeGateway();
|
||||
void closeGateway();
|
||||
setTakeover((current) => ({
|
||||
...current,
|
||||
active: false,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -107,7 +107,6 @@ export const useSidebar = () => {
|
||||
refreshStateRef.current = actions.refreshState;
|
||||
const sessionActions = createPicoclawSidebarSessionActions({
|
||||
t,
|
||||
runtimeStatus,
|
||||
transportState,
|
||||
runState,
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user