mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
Control UI explicit action feedback
Add explicit Control UI feedback for repeated actions: session switches now announce through the chat controls live-status path and flash the active session selector, config actions show inline busy state, and session list empty states distinguish filtered results with a Show all reset. Also refresh generated Control UI locale metadata and fallback markers.
This commit is contained in:
@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/trajectory: bound runtime trajectory capture and yield queued sidecar writes so oversized traces stop recording instead of monopolizing Gateway cleanup. Fixes #77124. Thanks @loyur.
|
||||
- Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis.
|
||||
- UI/chat: remove the unsupported `line-clamp` declaration from the chat queue text rule to eliminate Firefox console noise without changing visible truncation behavior. Thanks @ZanderH-code.
|
||||
- Control UI: add explicit feedback for repeated actions by announcing session switches, flashing the active session selector, showing inline Save/Apply/Update progress, and distinguishing filtered-empty session lists from genuinely empty session stores. Thanks @BunsDev.
|
||||
- Agents/Pi: suppress persistence for synthetic mid-turn overflow continuation prompts, so transcript-retry recovery does not write the "continue from transcript" prompt as a new user turn. Thanks @vincentkoc.
|
||||
- Agents/tools: strip reasoning text from visible rich presentation titles, blocks, buttons, and select labels before message-tool sends, so structured channel payloads cannot leak hidden planning. Thanks @vincentkoc.
|
||||
- Telegram: keep reply-dispatch lazy provider runtime chunks behind stable dist names and delete `/reasoning stream` previews after final delivery so package updates and live reasoning drafts do not leave Telegram turns broken or noisy. Thanks @BunsDev.
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:59.541Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:48.100Z",
|
||||
"locale": "ar",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:57.765Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:46.702Z",
|
||||
"locale": "de",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:58.121Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:46.979Z",
|
||||
"locale": "es",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:21.069Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:50.613Z",
|
||||
"locale": "fa",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:59.152Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:47.819Z",
|
||||
"locale": "fr",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:19.311Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:49.215Z",
|
||||
"locale": "id",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:59.895Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:48.376Z",
|
||||
"locale": "it",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:58.455Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:47.256Z",
|
||||
"locale": "ja-JP",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:58.813Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:47.535Z",
|
||||
"locale": "ko",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:20.725Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:50.321Z",
|
||||
"locale": "nl",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:19.638Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:49.488Z",
|
||||
"locale": "pl",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:57.413Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:46.427Z",
|
||||
"locale": "pt-BR",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -309,6 +309,13 @@
|
||||
"path": "ui/src/ui/chat/session-controls.ts",
|
||||
"text": "Chat model"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-attribute",
|
||||
"name": "aria-label",
|
||||
"path": "ui/src/ui/chat/session-controls.ts",
|
||||
"text": "Chat session"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-attribute",
|
||||
@@ -316,6 +323,13 @@
|
||||
"path": "ui/src/ui/chat/session-controls.ts",
|
||||
"text": "Chat thinking level"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-attribute",
|
||||
"name": "aria-label",
|
||||
"path": "ui/src/ui/chat/session-controls.ts",
|
||||
"text": "Filter sessions by agent"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"kind": "html-attribute",
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:19.987Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:49.767Z",
|
||||
"locale": "th",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:27:00.334Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:48.658Z",
|
||||
"locale": "tr",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:18.980Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:48.943Z",
|
||||
"locale": "uk",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:44:20.379Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:50.047Z",
|
||||
"locale": "vi",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:56.358Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:45.830Z",
|
||||
"locale": "zh-CN",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"fallbackKeys": [],
|
||||
"generatedAt": "2026-05-04T07:26:57.083Z",
|
||||
"fallbackKeys": [
|
||||
"chat.switchedSession",
|
||||
"sessionsView.noSessionsMatchFilters",
|
||||
"sessionsView.showAll"
|
||||
],
|
||||
"generatedAt": "2026-05-04T08:21:46.149Z",
|
||||
"locale": "zh-TW",
|
||||
"model": "gpt-5.5",
|
||||
"provider": "openai",
|
||||
"sourceHash": "9924252f06872c0217b810e072249991a908cda8329977622a2afd16f846fcd6",
|
||||
"totalKeys": 1001,
|
||||
"sourceHash": "926c835b1e931594ec63598a966c91906ca98425cc6bd89fe9787668bd442c01",
|
||||
"totalKeys": 1004,
|
||||
"translatedKeys": 1001,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
@@ -189,6 +189,8 @@ export const ar: TranslationMap = {
|
||||
verbose: "مطوّل",
|
||||
reasoning: "الاستدلال",
|
||||
noSessions: "لم يتم العثور على جلسات.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "وراثة",
|
||||
defaultOption: "الافتراضي ({value})",
|
||||
offExplicit: "إيقاف (صريح)",
|
||||
@@ -925,6 +927,7 @@ export const ar: TranslationMap = {
|
||||
updating: "جارٍ التحديث…",
|
||||
updateNow: "التحديث الآن",
|
||||
dismissUpdateBanner: "إغلاق لافتة التحديث",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English (الإنجليزية)",
|
||||
|
||||
@@ -193,6 +193,8 @@ export const de: TranslationMap = {
|
||||
verbose: "Ausführlich",
|
||||
reasoning: "Reasoning",
|
||||
noSessions: "Keine Sitzungen gefunden.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "Übernehmen",
|
||||
defaultOption: "Standard ({value})",
|
||||
offExplicit: "Aus (explizit)",
|
||||
@@ -939,6 +941,7 @@ export const de: TranslationMap = {
|
||||
updating: "Wird aktualisiert…",
|
||||
updateNow: "Jetzt aktualisieren",
|
||||
dismissUpdateBanner: "Update-Banner ausblenden",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Englisch",
|
||||
|
||||
@@ -188,6 +188,8 @@ export const en: TranslationMap = {
|
||||
verbose: "Verbose",
|
||||
reasoning: "Reasoning",
|
||||
noSessions: "No sessions found.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "inherit",
|
||||
defaultOption: "Default ({value})",
|
||||
offExplicit: "off (explicit)",
|
||||
@@ -926,6 +928,7 @@ export const en: TranslationMap = {
|
||||
updating: "Updating…",
|
||||
updateNow: "Update now",
|
||||
dismissUpdateBanner: "Dismiss update banner",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English",
|
||||
|
||||
@@ -190,6 +190,8 @@ export const es: TranslationMap = {
|
||||
verbose: "Detallado",
|
||||
reasoning: "Razonamiento",
|
||||
noSessions: "No se encontraron sesiones.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "heredar",
|
||||
defaultOption: "Predeterminado ({value})",
|
||||
offExplicit: "desactivado (explícito)",
|
||||
@@ -938,6 +940,7 @@ export const es: TranslationMap = {
|
||||
updating: "Actualizando…",
|
||||
updateNow: "Actualizar ahora",
|
||||
dismissUpdateBanner: "Descartar banner de actualización",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Inglés (English)",
|
||||
|
||||
@@ -191,6 +191,8 @@ export const fa: TranslationMap = {
|
||||
verbose: "پرگویی",
|
||||
reasoning: "استدلال",
|
||||
noSessions: "هیچ نشستی پیدا نشد.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "ارثبری",
|
||||
defaultOption: "پیشفرض ({value})",
|
||||
offExplicit: "خاموش (صریح)",
|
||||
@@ -934,6 +936,7 @@ export const fa: TranslationMap = {
|
||||
updating: "در حال بهروزرسانی…",
|
||||
updateNow: "اکنون بهروزرسانی کن",
|
||||
dismissUpdateBanner: "بستن بنر بهروزرسانی",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English (انگلیسی)",
|
||||
|
||||
@@ -192,6 +192,8 @@ export const fr: TranslationMap = {
|
||||
verbose: "Détaillé",
|
||||
reasoning: "Raisonnement",
|
||||
noSessions: "Aucune session trouvée.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "hériter",
|
||||
defaultOption: "Par défaut ({value})",
|
||||
offExplicit: "désactivé (explicite)",
|
||||
@@ -940,6 +942,7 @@ export const fr: TranslationMap = {
|
||||
updating: "Mise à jour…",
|
||||
updateNow: "Mettre à jour maintenant",
|
||||
dismissUpdateBanner: "Ignorer la bannière de mise à jour",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Anglais",
|
||||
|
||||
@@ -190,6 +190,8 @@ export const id: TranslationMap = {
|
||||
verbose: "Verbose",
|
||||
reasoning: "Penalaran",
|
||||
noSessions: "Tidak ada sesi yang ditemukan.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "warisi",
|
||||
defaultOption: "Default ({value})",
|
||||
offExplicit: "nonaktif (eksplisit)",
|
||||
@@ -933,6 +935,7 @@ export const id: TranslationMap = {
|
||||
updating: "Memperbarui…",
|
||||
updateNow: "Perbarui sekarang",
|
||||
dismissUpdateBanner: "Tutup banner pembaruan",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Inggris",
|
||||
|
||||
@@ -190,6 +190,8 @@ export const it: TranslationMap = {
|
||||
verbose: "Dettagliato",
|
||||
reasoning: "Ragionamento",
|
||||
noSessions: "Nessuna sessione trovata.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "eredita",
|
||||
defaultOption: "Predefinito ({value})",
|
||||
offExplicit: "disattivato (esplicito)",
|
||||
@@ -938,6 +940,7 @@ export const it: TranslationMap = {
|
||||
updating: "Aggiornamento…",
|
||||
updateNow: "Aggiorna ora",
|
||||
dismissUpdateBanner: "Ignora banner di aggiornamento",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English (Inglese)",
|
||||
|
||||
@@ -193,6 +193,8 @@ export const ja_JP: TranslationMap = {
|
||||
verbose: "詳細",
|
||||
reasoning: "推論",
|
||||
noSessions: "セッションが見つかりません。",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "継承",
|
||||
defaultOption: "デフォルト({value})",
|
||||
offExplicit: "オフ(明示)",
|
||||
@@ -936,6 +938,7 @@ export const ja_JP: TranslationMap = {
|
||||
updating: "更新中…",
|
||||
updateNow: "今すぐ更新",
|
||||
dismissUpdateBanner: "更新バナーを閉じる",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "英語",
|
||||
|
||||
@@ -189,6 +189,8 @@ export const ko: TranslationMap = {
|
||||
verbose: "상세",
|
||||
reasoning: "추론",
|
||||
noSessions: "세션을 찾을 수 없습니다.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "상속",
|
||||
defaultOption: "기본값({value})",
|
||||
offExplicit: "꺼짐(명시적)",
|
||||
@@ -929,6 +931,7 @@ export const ko: TranslationMap = {
|
||||
updating: "업데이트 중…",
|
||||
updateNow: "지금 업데이트",
|
||||
dismissUpdateBanner: "업데이트 배너 닫기",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "영어",
|
||||
|
||||
@@ -192,6 +192,8 @@ export const nl: TranslationMap = {
|
||||
verbose: "Uitgebreid",
|
||||
reasoning: "Redenering",
|
||||
noSessions: "Geen sessies gevonden.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "overnemen",
|
||||
defaultOption: "Standaard ({value})",
|
||||
offExplicit: "uit (expliciet)",
|
||||
@@ -936,6 +938,7 @@ export const nl: TranslationMap = {
|
||||
updating: "Bijwerken…",
|
||||
updateNow: "Nu bijwerken",
|
||||
dismissUpdateBanner: "Updatebanner sluiten",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English (Engels)",
|
||||
|
||||
@@ -191,6 +191,8 @@ export const pl: TranslationMap = {
|
||||
verbose: "Szczegółowo",
|
||||
reasoning: "Rozumowanie",
|
||||
noSessions: "Nie znaleziono sesji.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "dziedzicz",
|
||||
defaultOption: "Domyślnie ({value})",
|
||||
offExplicit: "wył. (jawnie)",
|
||||
@@ -939,6 +941,7 @@ export const pl: TranslationMap = {
|
||||
updating: "Aktualizowanie…",
|
||||
updateNow: "Aktualizuj teraz",
|
||||
dismissUpdateBanner: "Odrzuć baner aktualizacji",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Angielski (English)",
|
||||
|
||||
@@ -190,6 +190,8 @@ export const pt_BR: TranslationMap = {
|
||||
verbose: "Detalhado",
|
||||
reasoning: "Raciocínio",
|
||||
noSessions: "Nenhuma sessão encontrada.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "herdar",
|
||||
defaultOption: "Padrão ({value})",
|
||||
offExplicit: "desativado (explícito)",
|
||||
@@ -935,6 +937,7 @@ export const pt_BR: TranslationMap = {
|
||||
updating: "Atualizando…",
|
||||
updateNow: "Atualizar agora",
|
||||
dismissUpdateBanner: "Dispensar banner de atualização",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Inglês",
|
||||
|
||||
@@ -188,6 +188,8 @@ export const th: TranslationMap = {
|
||||
verbose: "ละเอียด",
|
||||
reasoning: "การให้เหตุผล",
|
||||
noSessions: "ไม่พบเซสชัน",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "สืบทอด",
|
||||
defaultOption: "ค่าเริ่มต้น ({value})",
|
||||
offExplicit: "ปิด (ระบุชัดเจน)",
|
||||
@@ -919,6 +921,7 @@ export const th: TranslationMap = {
|
||||
updating: "กำลังอัปเดต…",
|
||||
updateNow: "อัปเดตตอนนี้",
|
||||
dismissUpdateBanner: "ปิดแบนเนอร์อัปเดต",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "อังกฤษ",
|
||||
|
||||
@@ -192,6 +192,8 @@ export const tr: TranslationMap = {
|
||||
verbose: "Ayrıntılı",
|
||||
reasoning: "Akıl yürütme",
|
||||
noSessions: "Oturum bulunamadı.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "devral",
|
||||
defaultOption: "Varsayılan ({value})",
|
||||
offExplicit: "kapalı (açıkça)",
|
||||
@@ -938,6 +940,7 @@ export const tr: TranslationMap = {
|
||||
updating: "Güncelleniyor…",
|
||||
updateNow: "Şimdi güncelle",
|
||||
dismissUpdateBanner: "Güncelleme başlığını kapat",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "İngilizce",
|
||||
|
||||
@@ -191,6 +191,8 @@ export const uk: TranslationMap = {
|
||||
verbose: "Докладно",
|
||||
reasoning: "Міркування",
|
||||
noSessions: "Сеансів не знайдено.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "успадковувати",
|
||||
defaultOption: "За замовчуванням ({value})",
|
||||
offExplicit: "вимкнено (явно)",
|
||||
@@ -937,6 +939,7 @@ export const uk: TranslationMap = {
|
||||
updating: "Оновлення…",
|
||||
updateNow: "Оновити зараз",
|
||||
dismissUpdateBanner: "Закрити банер оновлення",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "Англійська",
|
||||
|
||||
@@ -190,6 +190,8 @@ export const vi: TranslationMap = {
|
||||
verbose: "Chi tiết",
|
||||
reasoning: "Suy luận",
|
||||
noSessions: "Không tìm thấy phiên nào.",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "kế thừa",
|
||||
defaultOption: "Mặc định ({value})",
|
||||
offExplicit: "tắt (rõ ràng)",
|
||||
@@ -930,6 +932,7 @@ export const vi: TranslationMap = {
|
||||
updating: "Đang cập nhật…",
|
||||
updateNow: "Cập nhật ngay",
|
||||
dismissUpdateBanner: "Bỏ qua banner cập nhật",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "English (Tiếng Anh)",
|
||||
|
||||
@@ -188,6 +188,8 @@ export const zh_CN: TranslationMap = {
|
||||
verbose: "详细",
|
||||
reasoning: "推理",
|
||||
noSessions: "未找到会话。",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "继承",
|
||||
defaultOption: "默认({value})",
|
||||
offExplicit: "关闭(显式)",
|
||||
@@ -918,6 +920,7 @@ export const zh_CN: TranslationMap = {
|
||||
updating: "正在更新…",
|
||||
updateNow: "立即更新",
|
||||
dismissUpdateBanner: "关闭更新横幅",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "英语",
|
||||
|
||||
@@ -188,6 +188,8 @@ export const zh_TW: TranslationMap = {
|
||||
verbose: "詳細",
|
||||
reasoning: "推理",
|
||||
noSessions: "找不到工作階段。",
|
||||
noSessionsMatchFilters: "No sessions match your filters.",
|
||||
showAll: "Show all",
|
||||
inherit: "繼承",
|
||||
defaultOption: "預設({value})",
|
||||
offExplicit: "關閉(明確)",
|
||||
@@ -919,6 +921,7 @@ export const zh_TW: TranslationMap = {
|
||||
updating: "正在更新…",
|
||||
updateNow: "立即更新",
|
||||
dismissUpdateBanner: "關閉更新橫幅",
|
||||
switchedSession: "Switched to {session}",
|
||||
},
|
||||
languages: {
|
||||
en: "英文",
|
||||
|
||||
@@ -1072,6 +1072,39 @@
|
||||
grid-area: agent;
|
||||
}
|
||||
|
||||
.chat-controls__session-row--flash .chat-controls__session-picker select {
|
||||
animation: chat-session-switch-flash 0.2s ease-out;
|
||||
}
|
||||
|
||||
.chat-controls__session-notice {
|
||||
width: 100%;
|
||||
min-height: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@keyframes chat-session-switch-flash {
|
||||
0% {
|
||||
border-color: color-mix(in srgb, var(--accent) 72%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent-subtle) 62%, var(--card));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
}
|
||||
100% {
|
||||
border-color: var(--border);
|
||||
background: var(--card);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat-controls__session-row--flash .chat-controls__session-picker select {
|
||||
animation: none;
|
||||
border-color: color-mix(in srgb, var(--accent) 70%, var(--border));
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-controls__model {
|
||||
grid-area: model;
|
||||
min-width: 0;
|
||||
|
||||
@@ -2509,6 +2509,20 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.data-table-empty-cell {
|
||||
padding: 46px 16px !important;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-table-empty-state {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
transition: background var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
@@ -242,6 +242,26 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.config-action-spinner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.config-action-spinner svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.config-action-spinner svg {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.config-changes-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
|
||||
@@ -820,7 +820,18 @@ describe("switchChatSession", () => {
|
||||
sessionsShowArchived: false,
|
||||
chatSideResultTerminalRuns: new Set(["btw-run-1"]),
|
||||
chatStreamStartedAt: 1,
|
||||
sessionsResult: {
|
||||
ts: 0,
|
||||
path: "",
|
||||
count: 2,
|
||||
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
|
||||
sessions: [
|
||||
row({ key: "main" }),
|
||||
row({ key: "agent:main:test-b", label: "Review Session" }),
|
||||
],
|
||||
},
|
||||
settings,
|
||||
announceSessionSwitch: vi.fn(),
|
||||
applySettings(next: typeof settings) {
|
||||
state.settings = next;
|
||||
},
|
||||
@@ -861,6 +872,10 @@ describe("switchChatSession", () => {
|
||||
includeUnknown: true,
|
||||
showArchived: false,
|
||||
});
|
||||
expect(
|
||||
(state as unknown as { announceSessionSwitch: ReturnType<typeof vi.fn> })
|
||||
.announceSessionSwitch,
|
||||
).toHaveBeenCalledWith("agent:main:test-b", "Review Session");
|
||||
});
|
||||
|
||||
it("restores queued messages when switching back to their session", async () => {
|
||||
@@ -886,6 +901,7 @@ describe("switchChatSession", () => {
|
||||
chatSideResultTerminalRuns: new Set<string>(),
|
||||
chatStreamStartedAt: 1,
|
||||
settings,
|
||||
announceSessionSwitch: vi.fn(),
|
||||
applySettings(next: typeof settings) {
|
||||
state.settings = next;
|
||||
},
|
||||
@@ -931,6 +947,7 @@ describe("switchChatSession", () => {
|
||||
chatSideResultTerminalRuns: new Set<string>(),
|
||||
chatStreamStartedAt: null,
|
||||
settings,
|
||||
announceSessionSwitch: vi.fn(),
|
||||
applySettings(next: typeof settings) {
|
||||
state.settings = next;
|
||||
},
|
||||
@@ -949,6 +966,10 @@ describe("switchChatSession", () => {
|
||||
switchChatSession(state, "main");
|
||||
await Promise.resolve();
|
||||
|
||||
expect(
|
||||
(state as unknown as { announceSessionSwitch: ReturnType<typeof vi.fn> })
|
||||
.announceSessionSwitch,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(refreshSlashCommandsMock).toHaveBeenCalledWith({
|
||||
client: state.client,
|
||||
agentId: undefined,
|
||||
|
||||
@@ -574,7 +574,13 @@ export function renderChatMobileToggle(state: AppViewState) {
|
||||
}
|
||||
|
||||
export function switchChatSession(state: AppViewState, nextSessionKey: string) {
|
||||
const previousSessionKey = state.sessionKey;
|
||||
const nextSessionRow = state.sessionsResult?.sessions.find((row) => row.key === nextSessionKey);
|
||||
const nextSessionLabel = resolveSessionDisplayName(nextSessionKey, nextSessionRow);
|
||||
resetChatStateForSessionSwitch(state, nextSessionKey);
|
||||
if (previousSessionKey !== nextSessionKey) {
|
||||
state.announceSessionSwitch?.(nextSessionKey, nextSessionLabel);
|
||||
}
|
||||
void state.loadAssistantIdentity();
|
||||
void refreshChatAvatar(state);
|
||||
void refreshSlashCommands({
|
||||
|
||||
@@ -1721,6 +1721,23 @@ export function renderApp(state: AppViewState) {
|
||||
onToggleFiltersCollapsed: () => {
|
||||
state.sessionsFiltersCollapsed = !state.sessionsFiltersCollapsed;
|
||||
},
|
||||
onClearFilters: () => {
|
||||
state.sessionsFilterActive = "";
|
||||
state.sessionsFilterLimit = "";
|
||||
state.sessionsIncludeGlobal = true;
|
||||
state.sessionsIncludeUnknown = true;
|
||||
state.sessionsShowArchived = true;
|
||||
state.sessionsSearchQuery = "";
|
||||
state.sessionsSelectedKeys = new Set();
|
||||
state.sessionsPage = 0;
|
||||
void loadSessions(state, {
|
||||
activeMinutes: 0,
|
||||
limit: 0,
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
showArchived: true,
|
||||
});
|
||||
},
|
||||
onSearchChange: (q) => {
|
||||
state.sessionsSearchQuery = q;
|
||||
state.sessionsPage = 0;
|
||||
|
||||
@@ -108,6 +108,9 @@ export type AppViewState = {
|
||||
chatModelOverrides: Record<string, ChatModelOverride | null>;
|
||||
chatModelsLoading: boolean;
|
||||
chatModelCatalog: ModelCatalogEntry[];
|
||||
sessionSwitchNotice: { id: number; text: string } | null;
|
||||
sessionSwitchFlashKey: string | null;
|
||||
announceSessionSwitch?: (sessionKey: string, label: string) => void;
|
||||
chatQueue: ChatQueueItem[];
|
||||
chatQueueBySession: Record<string, ChatQueueItem[]>;
|
||||
chatLocalInputHistoryBySession: Record<string, Array<{ text: string; ts: number }>>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import { i18n, I18nController, isSupportedLocale } from "../i18n/index.ts";
|
||||
import { i18n, I18nController, isSupportedLocale, t } from "../i18n/index.ts";
|
||||
import {
|
||||
handleChannelConfigReload as handleChannelConfigReloadInternal,
|
||||
handleChannelConfigSave as handleChannelConfigSaveInternal,
|
||||
@@ -217,6 +217,11 @@ export class OpenClawApp extends LitElement {
|
||||
@state() chatModelOverrides: Record<string, ChatModelOverride | null> = {};
|
||||
@state() chatModelsLoading = false;
|
||||
@state() chatModelCatalog: ModelCatalogEntry[] = [];
|
||||
@state() sessionSwitchNotice: { id: number; text: string } | null = null;
|
||||
@state() sessionSwitchFlashKey: string | null = null;
|
||||
private sessionSwitchNoticeSeq = 0;
|
||||
private sessionSwitchNoticeTimer: number | null = null;
|
||||
private sessionSwitchFlashTimer: number | null = null;
|
||||
@state() chatQueue: ChatQueueItem[] = [];
|
||||
@state() chatQueueBySession: Record<string, ChatQueueItem[]> = {};
|
||||
@state() chatAttachments: ChatAttachment[] = [];
|
||||
@@ -651,6 +656,14 @@ export class OpenClawApp extends LitElement {
|
||||
document.removeEventListener("keydown", this.globalKeydownHandler);
|
||||
document.removeEventListener("keydown", this.chatMobileControlsKeydownHandler);
|
||||
document.removeEventListener("pointerdown", this.chatMobileControlsPointerdownHandler);
|
||||
if (this.sessionSwitchNoticeTimer !== null) {
|
||||
window.clearTimeout(this.sessionSwitchNoticeTimer);
|
||||
this.sessionSwitchNoticeTimer = null;
|
||||
}
|
||||
if (this.sessionSwitchFlashTimer !== null) {
|
||||
window.clearTimeout(this.sessionSwitchFlashTimer);
|
||||
this.sessionSwitchFlashTimer = null;
|
||||
}
|
||||
this.chatMobileControlsTrigger = null;
|
||||
handleDisconnected(this as unknown as Parameters<typeof handleDisconnected>[0]);
|
||||
super.disconnectedCallback();
|
||||
@@ -852,6 +865,33 @@ export class OpenClawApp extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
announceSessionSwitch(sessionKey: string, label: string) {
|
||||
const id = ++this.sessionSwitchNoticeSeq;
|
||||
if (this.sessionSwitchNoticeTimer !== null) {
|
||||
window.clearTimeout(this.sessionSwitchNoticeTimer);
|
||||
}
|
||||
if (this.sessionSwitchFlashTimer !== null) {
|
||||
window.clearTimeout(this.sessionSwitchFlashTimer);
|
||||
}
|
||||
this.sessionSwitchNotice = {
|
||||
id,
|
||||
text: t("chat.switchedSession", { session: label }),
|
||||
};
|
||||
this.sessionSwitchFlashKey = sessionKey;
|
||||
this.sessionSwitchFlashTimer = window.setTimeout(() => {
|
||||
if (this.sessionSwitchNotice?.id === id) {
|
||||
this.sessionSwitchFlashKey = null;
|
||||
}
|
||||
this.sessionSwitchFlashTimer = null;
|
||||
}, 200);
|
||||
this.sessionSwitchNoticeTimer = window.setTimeout(() => {
|
||||
if (this.sessionSwitchNotice?.id === id) {
|
||||
this.sessionSwitchNotice = null;
|
||||
}
|
||||
this.sessionSwitchNoticeTimer = null;
|
||||
}, 2800);
|
||||
}
|
||||
|
||||
buildThemeOrder(active: ThemeName): ThemeName[] {
|
||||
const all = [...VALID_THEME_NAMES];
|
||||
const rest = all.filter((id) => id !== active);
|
||||
|
||||
@@ -37,12 +37,16 @@ export function renderChatSessionSelect(
|
||||
const selectedSessionLabel =
|
||||
sessionGroups.flatMap((group) => group.options).find((entry) => entry.key === state.sessionKey)
|
||||
?.label ?? state.sessionKey;
|
||||
const flashSession = state.sessionSwitchFlashKey === state.sessionKey;
|
||||
const rowClass = [
|
||||
"chat-controls__session-row",
|
||||
hasAgentSelect ? "" : "chat-controls__session-row--single-agent",
|
||||
flashSession ? "chat-controls__session-row--flash" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return html`
|
||||
<div
|
||||
class=${hasAgentSelect
|
||||
? "chat-controls__session-row"
|
||||
: "chat-controls__session-row chat-controls__session-row--single-agent"}
|
||||
>
|
||||
<div class=${rowClass}>
|
||||
${agentSelect}
|
||||
<label class="field chat-controls__session chat-controls__session-picker">
|
||||
<select
|
||||
@@ -82,6 +86,9 @@ export function renderChatSessionSelect(
|
||||
</label>
|
||||
${modelSelect} ${thinkingSelect}
|
||||
</div>
|
||||
<div class="chat-controls__session-notice" role="status" aria-live="polite">
|
||||
${state.sessionSwitchNotice?.text ?? ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -873,6 +873,21 @@ describe("chat session controls", () => {
|
||||
expect(onSwitchSession).toHaveBeenCalledWith(state, "agent:beta:main");
|
||||
});
|
||||
|
||||
it("renders session switch feedback in the chat controls live region", () => {
|
||||
const { state } = createChatHeaderState();
|
||||
state.sessionSwitchNotice = { id: 1, text: "Switched to Coding" };
|
||||
state.sessionSwitchFlashKey = state.sessionKey;
|
||||
|
||||
const container = document.createElement("div");
|
||||
render(renderChatSessionSelect(state), container);
|
||||
|
||||
const notice = container.querySelector<HTMLElement>(".chat-controls__session-notice");
|
||||
expect(notice?.getAttribute("role")).toBe("status");
|
||||
expect(notice?.getAttribute("aria-live")).toBe("polite");
|
||||
expect(notice?.textContent?.trim()).toBe("Switched to Coding");
|
||||
expect(container.querySelector(".chat-controls__session-row--flash")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the active agent main session instead of a blank select when no row exists yet", () => {
|
||||
const { state } = createChatHeaderState();
|
||||
state.sessionKey = "agent:main:main";
|
||||
|
||||
@@ -65,12 +65,14 @@ describe("config view", () => {
|
||||
clearButton?: HTMLButtonElement;
|
||||
saveButton?: HTMLButtonElement;
|
||||
applyButton?: HTMLButtonElement;
|
||||
updateButton?: HTMLButtonElement;
|
||||
} {
|
||||
const buttons = Array.from(container.querySelectorAll("button"));
|
||||
return {
|
||||
clearButton: buttons.find((btn) => btn.textContent?.trim() === "Clear"),
|
||||
saveButton: buttons.find((btn) => btn.textContent?.trim() === "Save"),
|
||||
applyButton: buttons.find((btn) => btn.textContent?.trim() === "Apply"),
|
||||
updateButton: buttons.find((btn) => btn.textContent?.trim() === "Update"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,6 +173,58 @@ describe("config view", () => {
|
||||
expect(onReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders inline progress inside busy action buttons without locking adjacent controls", () => {
|
||||
const container = document.createElement("div");
|
||||
const renderCase = (overrides: Partial<ConfigProps>) =>
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
gateway: { type: "object", properties: { mode: { type: "string" } } },
|
||||
},
|
||||
},
|
||||
formValue: { gateway: { mode: "remote" } },
|
||||
originalValue: { gateway: { mode: "local" } },
|
||||
...overrides,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
renderCase({ saving: true });
|
||||
let busyButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Saving…"),
|
||||
);
|
||||
let { clearButton, applyButton } = findActionButtons(container);
|
||||
expect(busyButton).toBeTruthy();
|
||||
expect(busyButton?.disabled).toBe(true);
|
||||
expect(busyButton?.getAttribute("aria-busy")).toBe("true");
|
||||
expect(busyButton?.querySelector(".config-action-spinner")).not.toBeNull();
|
||||
expect(clearButton?.disabled).toBe(false);
|
||||
expect(applyButton?.disabled).toBe(false);
|
||||
|
||||
renderCase({ applying: true });
|
||||
busyButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Applying…"),
|
||||
);
|
||||
({ clearButton } = findActionButtons(container));
|
||||
expect(busyButton).toBeTruthy();
|
||||
expect(busyButton?.disabled).toBe(true);
|
||||
expect(busyButton?.querySelector(".config-action-spinner")).not.toBeNull();
|
||||
expect(clearButton?.disabled).toBe(false);
|
||||
|
||||
renderCase({ updating: true });
|
||||
busyButton = Array.from(container.querySelectorAll("button")).find((button) =>
|
||||
button.textContent?.includes("Updating…"),
|
||||
);
|
||||
({ clearButton } = findActionButtons(container));
|
||||
expect(busyButton).toBeTruthy();
|
||||
expect(busyButton?.disabled).toBe(true);
|
||||
expect(busyButton?.querySelector(".config-action-spinner")).not.toBeNull();
|
||||
expect(clearButton?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("switches mode via the sidebar toggle", () => {
|
||||
const container = document.createElement("div");
|
||||
const onFormModeChange = vi.fn();
|
||||
|
||||
@@ -1377,6 +1377,11 @@ export function renderConfig(props: ConfigProps) {
|
||||
hasChanges &&
|
||||
(formMode === "raw" ? true : canSaveForm);
|
||||
const canUpdate = props.connected && !props.applying && !props.updating;
|
||||
const renderActionButtonContent = (busy: boolean, label: string, busyLabel: string) =>
|
||||
busy
|
||||
? html`<span class="config-action-spinner" aria-hidden="true">${icons.loader}</span
|
||||
>${busyLabel}`
|
||||
: label;
|
||||
|
||||
const showAppearanceOnRoot =
|
||||
includeVirtualSections &&
|
||||
@@ -1449,14 +1454,29 @@ export function renderConfig(props: ConfigProps) {
|
||||
<button class="btn btn--sm" ?disabled=${!hasChanges} @click=${props.onReset}>
|
||||
Clear
|
||||
</button>
|
||||
<button class="btn btn--sm primary" ?disabled=${!canSave} @click=${props.onSave}>
|
||||
${props.saving ? "Saving…" : "Save"}
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${!canSave}
|
||||
aria-busy=${props.saving ? "true" : "false"}
|
||||
@click=${props.onSave}
|
||||
>
|
||||
${renderActionButtonContent(props.saving, "Save", "Saving…")}
|
||||
</button>
|
||||
<button class="btn btn--sm" ?disabled=${!canApply} @click=${props.onApply}>
|
||||
${props.applying ? "Applying…" : "Apply"}
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!canApply}
|
||||
aria-busy=${props.applying ? "true" : "false"}
|
||||
@click=${props.onApply}
|
||||
>
|
||||
${renderActionButtonContent(props.applying, "Apply", "Applying…")}
|
||||
</button>
|
||||
<button class="btn btn--sm" ?disabled=${!canUpdate} @click=${props.onUpdate}>
|
||||
${props.updating ? "Updating…" : "Update"}
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!canUpdate}
|
||||
aria-busy=${props.updating ? "true" : "false"}
|
||||
@click=${props.onUpdate}
|
||||
>
|
||||
${renderActionButtonContent(props.updating, "Update", "Updating…")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,6 +51,7 @@ function buildProps(result: SessionsListResult): SessionsProps {
|
||||
checkpointErrorByKey: {},
|
||||
onFiltersChange: () => undefined,
|
||||
onToggleFiltersCollapsed: () => undefined,
|
||||
onClearFilters: () => undefined,
|
||||
onSearchChange: () => undefined,
|
||||
onSortChange: () => undefined,
|
||||
onPageChange: () => undefined,
|
||||
@@ -565,4 +566,53 @@ describe("sessions view", () => {
|
||||
expect(onDeselectAll).not.toHaveBeenCalled();
|
||||
expect(onSelectPage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a reset action when filters hide every session", async () => {
|
||||
const container = document.createElement("div");
|
||||
const onClearFilters = vi.fn();
|
||||
render(
|
||||
renderSessions({
|
||||
...buildProps(
|
||||
buildMultiResult([
|
||||
{
|
||||
key: "agent:main:main",
|
||||
kind: "direct",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
]),
|
||||
),
|
||||
searchQuery: "missing",
|
||||
onClearFilters,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(container.textContent).toContain("No sessions match your filters.");
|
||||
const showAll = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.trim() === "Show all",
|
||||
);
|
||||
expect(showAll).toBeTruthy();
|
||||
showAll?.click();
|
||||
expect(onClearFilters).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the plain empty state when no filters are active", async () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderSessions({
|
||||
...buildProps(buildMultiResult([])),
|
||||
activeMinutes: "",
|
||||
limit: "",
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
showArchived: true,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(container.textContent).toContain("No sessions found.");
|
||||
expect(container.textContent).not.toContain("Show all");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ export type SessionsProps = {
|
||||
showArchived: boolean;
|
||||
}) => void;
|
||||
onToggleFiltersCollapsed: () => void;
|
||||
onClearFilters: () => void;
|
||||
onSearchChange: (query: string) => void;
|
||||
onSortChange: (column: "key" | "kind" | "updated" | "tokens", dir: "asc" | "desc") => void;
|
||||
onPageChange: (page: number) => void;
|
||||
@@ -225,6 +226,22 @@ function paginateRows<T>(rows: T[], page: number, pageSize: number): T[] {
|
||||
return rows.slice(start, start + pageSize);
|
||||
}
|
||||
|
||||
function hasPositiveNumberFilter(value: string): boolean {
|
||||
const parsed = Number(value.trim());
|
||||
return Number.isFinite(parsed) && parsed > 0;
|
||||
}
|
||||
|
||||
function hasActiveFilters(props: SessionsProps): boolean {
|
||||
return (
|
||||
normalizeLowercaseStringOrEmpty(props.searchQuery).length > 0 ||
|
||||
hasPositiveNumberFilter(props.activeMinutes) ||
|
||||
hasPositiveNumberFilter(props.limit) ||
|
||||
!props.includeGlobal ||
|
||||
!props.includeUnknown ||
|
||||
!props.showArchived
|
||||
);
|
||||
}
|
||||
|
||||
function formatCheckpointReason(reason: SessionCompactionCheckpoint["reason"]): string {
|
||||
switch (reason) {
|
||||
case "manual":
|
||||
@@ -304,6 +321,8 @@ export function renderSessions(props: SessionsProps) {
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / props.pageSize));
|
||||
const page = Math.min(props.page, totalPages - 1);
|
||||
const paginated = paginateRows(sorted, page, props.pageSize);
|
||||
const emptyBecauseFiltered =
|
||||
rawRows.length === 0 ? hasActiveFilters(props) : filtered.length === 0;
|
||||
const activeTooltip = t("sessionsView.activeTooltip", { count: props.activeMinutes.trim() });
|
||||
const limitTooltip = t("sessionsView.limitTooltip");
|
||||
const globalTooltip = t("sessionsView.globalTooltip");
|
||||
@@ -537,11 +556,17 @@ export function renderSessions(props: SessionsProps) {
|
||||
${paginated.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
colspan="11"
|
||||
style="text-align: center; padding: 48px 16px; color: var(--muted)"
|
||||
>
|
||||
${t("sessionsView.noSessions")}
|
||||
<td colspan="11" class="data-table-empty-cell">
|
||||
${emptyBecauseFiltered
|
||||
? html`
|
||||
<div class="data-table-empty-state" role="status" aria-live="polite">
|
||||
<div>${t("sessionsView.noSessionsMatchFilters")}</div>
|
||||
<button class="btn btn--sm" @click=${props.onClearFilters}>
|
||||
${t("sessionsView.showAll")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: t("sessionsView.noSessions")}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user