feat(web): make WebChat commentary persistence configurable

Add a per-viewer 'Keep commentary' toggle (UiSettings.chatPersistCommentary,
default true) that controls whether keyed Codex preamble/commentary blocks
stay after the final answer or clear with it.

- Persist (default): keyed commentary materializes as durable blocks, current
  behavior, existing proof unchanged.
- Transient (toggle off): commentary stays live during streaming but is never
  materialized, so it disappears as the final message arrives. This is the
  transient-only behavior from #92236, now user-selectable instead of a
  maintainer-level either/or policy choice.

Single gating point in materializeVisibleStreamState (skip itemId-keyed parts
when persistCommentary is false); threaded from settings through the chat
event handler. Adds desktop + mobile header toggles and an en.ts label
(locale bundles regenerated via ui:i18n:sync, English fallback).

Tests: reconciliation persist/transient coverage, final-event handler honors
the setting, settings round-trip + header button assertions updated.
This commit is contained in:
ragesaq
2026-06-24 07:16:26 +00:00
committed by Ayaan Zaidi
parent 8af9bf9264
commit 789e0f939b
50 changed files with 345 additions and 82 deletions
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:07:21.796Z",
"generatedAt": "2026-06-29T17:06:18.945Z",
"locale": "ar",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:06:02.374Z",
"generatedAt": "2026-06-29T17:06:17.867Z",
"locale": "de",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:06:31.127Z",
"generatedAt": "2026-06-29T17:06:18.056Z",
"locale": "es",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:50.278Z",
"generatedAt": "2026-06-29T17:06:20.668Z",
"locale": "fa",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:06:59.616Z",
"generatedAt": "2026-06-29T17:06:18.559Z",
"locale": "fr",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+7 -4
View File
@@ -1,11 +1,14 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T21:38:17.797Z",
"fallbackKeys": [
"chat.commentaryLabel",
"chat.commentaryToggle"
],
"generatedAt": "2026-06-29T17:06:18.728Z",
"locale": "hi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1416,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:00.729Z",
"generatedAt": "2026-06-29T17:06:19.737Z",
"locale": "id",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:07:12.745Z",
"generatedAt": "2026-06-29T17:06:19.156Z",
"locale": "it",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:06:34.355Z",
"generatedAt": "2026-06-29T17:06:18.241Z",
"locale": "ja-JP",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:06:42.132Z",
"generatedAt": "2026-06-29T17:06:18.409Z",
"locale": "ko",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:47.496Z",
"generatedAt": "2026-06-29T17:06:20.490Z",
"locale": "nl",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:08.174Z",
"generatedAt": "2026-06-29T17:06:19.915Z",
"locale": "pl",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:05:51.685Z",
"generatedAt": "2026-06-29T17:06:17.682Z",
"locale": "pt-BR",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+14
View File
@@ -1513,6 +1513,13 @@
"path": "ui/src/ui/views/channels.whatsapp.ts",
"text": "WhatsApp"
},
{
"count": 1,
"kind": "html-attribute",
"name": "aria-label",
"path": "ui/src/ui/views/chat.ts",
"text": "Cancel reply"
},
{
"count": 1,
"kind": "html-attribute",
@@ -1597,6 +1604,13 @@
"path": "ui/src/ui/views/chat.ts",
"text": "Search messages..."
},
{
"count": 1,
"kind": "html-attribute",
"name": "title",
"path": "ui/src/ui/views/chat.ts",
"text": "Cancel reply"
},
{
"count": 1,
"kind": "html-attribute",
+7 -4
View File
@@ -1,11 +1,14 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T21:42:51.346Z",
"fallbackKeys": [
"chat.commentaryLabel",
"chat.commentaryToggle"
],
"generatedAt": "2026-06-29T17:06:20.849Z",
"locale": "ru",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1416,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:16.442Z",
"generatedAt": "2026-06-29T17:06:20.099Z",
"locale": "th",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:07:26.912Z",
"generatedAt": "2026-06-29T17:06:19.344Z",
"locale": "tr",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:07:50.082Z",
"generatedAt": "2026-06-29T17:06:19.546Z",
"locale": "uk",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:08:44.881Z",
"generatedAt": "2026-06-29T17:06:20.308Z",
"locale": "vi",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:05:51.877Z",
"generatedAt": "2026-06-29T17:06:17.341Z",
"locale": "zh-CN",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-06-26T22:05:54.449Z",
"generatedAt": "2026-06-29T17:06:17.519Z",
"locale": "zh-TW",
"model": "claude-opus-4-8",
"provider": "anthropic",
"sourceHash": "08e6ec5c2e8416d0049d2452906c20d096e17e384299af89cde872f0cfbb5015",
"totalKeys": 1416,
"translatedKeys": 1416,
"sourceHash": "29f546ff372524aa64d09bd700f76899045da99552fa295a32eda9fad2a0407f",
"totalKeys": 1418,
"translatedKeys": 1418,
"workflow": 1
}
+2
View File
@@ -1313,6 +1313,8 @@ export const ar: TranslationMap = {
settings: "إعدادات الدردشة",
thinkingToggle: "تبديل مخرجات تفكير/عمل المساعد",
toolCallsToggle: "تبديل استدعاءات الأدوات ونتائج الأدوات",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "وضع التمرير التلقائي",
autoScrollAlways: "دائمًا",
autoScrollNearBottom: "قرب الأسفل",
+2
View File
@@ -1340,6 +1340,8 @@ export const de: TranslationMap = {
settings: "Chat-Einstellungen",
thinkingToggle: "Ausgabe des Assistenten ein-/ausblenden",
toolCallsToggle: "Tool-Aufrufe und Tool-Ergebnisse umschalten",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Automatischer Scrollmodus",
autoScrollAlways: "Immer",
autoScrollNearBottom: "Nahe am unteren Rand",
+2
View File
@@ -1319,6 +1319,8 @@ export const en: TranslationMap = {
settings: "Chat settings",
thinkingToggle: "Toggle assistant thinking/working output",
toolCallsToggle: "Toggle tool calls and tool results",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Auto-scroll mode",
autoScrollAlways: "Always",
autoScrollNearBottom: "Near bottom",
+2
View File
@@ -1337,6 +1337,8 @@ export const es: TranslationMap = {
settings: "Configuración del chat",
thinkingToggle: "Alternar salida de pensamiento/trabajo del asistente",
toolCallsToggle: "Alternar llamadas a herramientas y resultados de herramientas",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Modo de desplazamiento automático",
autoScrollAlways: "Siempre",
autoScrollNearBottom: "Cerca del final",
+2
View File
@@ -1331,6 +1331,8 @@ export const fa: TranslationMap = {
settings: "تنظیمات چت",
thinkingToggle: "تغییر وضعیت خروجی فکر/کار دستیار",
toolCallsToggle: "تغییر وضعیت نمایش فراخوانی‌های ابزار و نتایج ابزار",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "حالت پیمایش خودکار",
autoScrollAlways: "همیشه",
autoScrollNearBottom: "نزدیک پایین",
+2
View File
@@ -1344,6 +1344,8 @@ export const fr: TranslationMap = {
settings: "Paramètres de chat",
thinkingToggle: "Afficher/masquer la sortie de réflexion/travail de lassistant",
toolCallsToggle: "Afficher/masquer les appels doutil et les résultats doutil",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Mode de défilement automatique",
autoScrollAlways: "Toujours",
autoScrollNearBottom: "Près du bas",
+2
View File
@@ -1315,6 +1315,8 @@ export const hi: TranslationMap = {
settings: "चैट सेटिंग्स",
thinkingToggle: "सहायक की सोच/कार्य आउटपुट टॉगल करें",
toolCallsToggle: "टूल कॉल और टूल परिणाम टॉगल करें",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "ऑटो-स्क्रॉल मोड",
autoScrollAlways: "हमेशा",
autoScrollNearBottom: "नीचे के पास",
+2
View File
@@ -1329,6 +1329,8 @@ export const id: TranslationMap = {
settings: "Pengaturan chat",
thinkingToggle: "Alihkan output berpikir/bekerja asisten",
toolCallsToggle: "Alihkan panggilan alat dan hasil alat",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Mode gulir otomatis",
autoScrollAlways: "Selalu",
autoScrollNearBottom: "Dekat bagian bawah",
+2
View File
@@ -1336,6 +1336,8 @@ export const it: TranslationMap = {
settings: "Impostazioni chat",
thinkingToggle: "Attiva/disattiva output di pensiero/elaborazione dell'assistente",
toolCallsToggle: "Attiva/disattiva chiamate agli strumenti e risultati strumenti",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Modalità di scorrimento automatico",
autoScrollAlways: "Sempre",
autoScrollNearBottom: "Vicino al fondo",
+2
View File
@@ -1334,6 +1334,8 @@ export const ja_JP: TranslationMap = {
settings: "チャット設定",
thinkingToggle: "アシスタントの思考 / 作業出力の表示を切り替え",
toolCallsToggle: "ツール呼び出しとツール結果の表示を切り替え",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "自動スクロールモード",
autoScrollAlways: "常に",
autoScrollNearBottom: "下部付近",
+2
View File
@@ -1320,6 +1320,8 @@ export const ko: TranslationMap = {
settings: "채팅 설정",
thinkingToggle: "어시스턴트 생각/작업 출력 전환",
toolCallsToggle: "도구 호출 및 도구 결과 전환",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "자동 스크롤 모드",
autoScrollAlways: "항상",
autoScrollNearBottom: "하단 근처",
+2
View File
@@ -1335,6 +1335,8 @@ export const nl: TranslationMap = {
settings: "Chatinstellingen",
thinkingToggle: "Denken-/werken-output van assistent schakelen",
toolCallsToggle: "Tool-aanroepen en toolresultaten schakelen",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Automatisch scrollen",
autoScrollAlways: "Altijd",
autoScrollNearBottom: "Bijna onderaan",
+2
View File
@@ -1334,6 +1334,8 @@ export const pl: TranslationMap = {
settings: "Ustawienia czatu",
thinkingToggle: "Przełącz wyświetlanie myślenia/pracy asystenta",
toolCallsToggle: "Przełącz wyświetlanie wywołań narzędzi i wyników narzędzi",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Tryb automatycznego przewijania",
autoScrollAlways: "Zawsze",
autoScrollNearBottom: "Blisko dołu",
+2
View File
@@ -1331,6 +1331,8 @@ export const pt_BR: TranslationMap = {
settings: "Configurações do chat",
thinkingToggle: "Alternar saída de pensamento/trabalho do assistente",
toolCallsToggle: "Alternar chamadas de ferramenta e resultados de ferramenta",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Modo de rolagem automática",
autoScrollAlways: "Sempre",
autoScrollNearBottom: "Próximo ao final",
+2
View File
@@ -1339,6 +1339,8 @@ export const ru: TranslationMap = {
settings: "Настройки чата",
thinkingToggle: "Показать или скрыть вывод размышлений/работы ассистента",
toolCallsToggle: "Показать или скрыть вызовы инструментов и их результаты",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Режим автопрокрутки",
autoScrollAlways: "Всегда",
autoScrollNearBottom: "Рядом с нижним краем",
+2
View File
@@ -1297,6 +1297,8 @@ export const th: TranslationMap = {
settings: "การตั้งค่าแชท",
thinkingToggle: "สลับการแสดงผลการคิด/การทำงานของผู้ช่วย",
toolCallsToggle: "สลับการแสดงการเรียกใช้ tool และผลลัพธ์ของ tool",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "โหมดเลื่อนอัตโนมัติ",
autoScrollAlways: "เสมอ",
autoScrollNearBottom: "ใกล้ด้านล่าง",
+2
View File
@@ -1335,6 +1335,8 @@ export const tr: TranslationMap = {
settings: "Sohbet ayarları",
thinkingToggle: "Asistanın düşünme/çalışma çıktısını aç/kapat",
toolCallsToggle: "Araç çağrılarını ve araç sonuçlarını aç/kapat",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Otomatik kaydırma modu",
autoScrollAlways: "Her zaman",
autoScrollNearBottom: "Alta yakın",
+2
View File
@@ -1333,6 +1333,8 @@ export const uk: TranslationMap = {
settings: "Налаштування чату",
thinkingToggle: "Перемкнути показ мислення/роботи асистента",
toolCallsToggle: "Перемкнути виклики інструментів і результати інструментів",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Режим автопрокручування",
autoScrollAlways: "Завжди",
autoScrollNearBottom: "Біля низу",
+2
View File
@@ -1321,6 +1321,8 @@ export const vi: TranslationMap = {
settings: "Cài đặt trò chuyện",
thinkingToggle: "Bật/tắt đầu ra suy nghĩ/đang xử lý của trợ lý",
toolCallsToggle: "Bật/tắt lượt gọi công cụ và kết quả công cụ",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "Chế độ tự động cuộn",
autoScrollAlways: "Luôn luôn",
autoScrollNearBottom: "Gần cuối",
+2
View File
@@ -1292,6 +1292,8 @@ export const zh_CN: TranslationMap = {
settings: "聊天设置",
thinkingToggle: "切换助手思考/工作输出",
toolCallsToggle: "切换工具调用和工具结果",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "自动滚动模式",
autoScrollAlways: "始终",
autoScrollNearBottom: "接近底部",
+2
View File
@@ -1294,6 +1294,8 @@ export const zh_TW: TranslationMap = {
settings: "聊天設定",
thinkingToggle: "切換助手思考/工作輸出",
toolCallsToggle: "切換工具呼叫與工具結果",
commentaryToggle: "Keep commentary after the final answer (off: clears with the final message)",
commentaryLabel: "Commentary",
autoScrollMode: "自動捲動模式",
autoScrollAlways: "一律",
autoScrollNearBottom: "接近底部",
+3 -2
View File
@@ -119,7 +119,7 @@ describe("chat header controls (browser)", () => {
),
);
expect(buttons).toHaveLength(5);
expect(buttons).toHaveLength(6);
const labels = buttons.map((button) => button.getAttribute("data-tooltip"));
expect(labels).toEqual([
@@ -127,6 +127,7 @@ describe("chat header controls (browser)", () => {
`${t("chat.autoScrollMode")}: ${t("chat.autoScrollNearBottom")}`,
t("chat.thinkingToggle"),
t("chat.toolCallsToggle"),
t("chat.commentaryToggle"),
t("chat.showCronSessions"),
]);
@@ -231,7 +232,7 @@ describe("chat header controls (browser)", () => {
container.querySelectorAll<HTMLButtonElement>(".chat-controls__thinking .btn--icon"),
);
expect(buttons).toHaveLength(4);
expect(buttons).toHaveLength(5);
const autoScrollButton = requireButton(buttons.at(0), "auto-scroll mode");
expect(autoScrollButton.dataset.chatAutoScrollMode).toBe("near-bottom");
const cronButton = requireButton(buttons.at(-1), "cron sessions");
+43
View File
@@ -365,12 +365,16 @@ export function renderChatControls(state: AppViewState) {
const disableThinkingToggle = state.onboarding;
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
const persistCommentary = state.settings.chatPersistCommentary !== false;
const thinkingLabel = disableThinkingToggle
? t("chat.onboardingDisabled")
: t("chat.thinkingToggle");
const toolCallsLabel = disableThinkingToggle
? t("chat.onboardingDisabled")
: t("chat.toolCallsToggle");
const commentaryLabel = disableThinkingToggle
? t("chat.onboardingDisabled")
: t("chat.commentaryToggle");
const refreshDisabled =
!state.connected ||
state.chatManualRefreshInFlight ||
@@ -503,6 +507,28 @@ export function renderChatControls(state: AppViewState) {
${toolCallsIcon}
<span class="chat-settings-action__text">${t("agents.tabs.tools")}</span>
</button>
<button
class="btn btn--sm btn--icon chat-settings-action ${persistCommentary
? "active"
: ""}"
?disabled=${disableThinkingToggle}
@click=${() => {
if (disableThinkingToggle) {
return;
}
state.applySettings({
...state.settings,
chatPersistCommentary: !persistCommentary,
});
}}
aria-pressed=${persistCommentary}
title=${commentaryLabel}
aria-label=${commentaryLabel}
data-tooltip=${commentaryLabel}
>
${persistCommentary ? icons.pin : icons.pinOff}
<span class="chat-settings-action__text">${t("chat.commentaryLabel")}</span>
</button>
<button
class="btn btn--sm btn--icon chat-settings-action ${hideCron ? "active" : ""}"
@click=${() => {
@@ -534,6 +560,7 @@ export function renderChatMobileToggle(state: AppViewState) {
const disableThinkingToggle = state.onboarding;
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
const persistCommentary = state.settings.chatPersistCommentary !== false;
const hideCron = state.sessionsHideCron ?? true;
const hiddenCronCount = hideCron ? countHiddenCronSessions(state, state.sessionsResult) : 0;
const toolCallsIcon = html`
@@ -627,6 +654,22 @@ export function renderChatMobileToggle(state: AppViewState) {
>
${toolCallsIcon}
</button>
<button
class="btn btn--sm btn--icon ${persistCommentary ? "active" : ""}"
?disabled=${disableThinkingToggle}
@click=${() => {
if (!disableThinkingToggle) {
state.applySettings({
...state.settings,
chatPersistCommentary: !persistCommentary,
});
}
}}
aria-pressed=${persistCommentary}
title=${t("chat.commentaryToggle")}
>
${persistCommentary ? icons.pin : icons.pinOff}
</button>
<button
class="btn btn--sm btn--icon ${hideCron ? "active" : ""}"
@click=${() => {
@@ -183,6 +183,72 @@ describe("stream reconciliation", () => {
expect(next.map(messageText)).toEqual(["latest ask", "before tool\nfinal answer"]);
});
it("omits keyed commentary parts when persistCommentary is false (transient mode)", () => {
const state = {
chatStream: null,
chatStreamStartedAt: null,
chatStreamSegments: [
{ text: "first preamble", ts: 2, itemId: "preamble-1" },
{ text: "second preamble", ts: 3, itemId: "preamble-2" },
],
} satisfies StreamReconciliationState & {
chatStreamSegments: Array<{ text: string; ts: number; itemId: string }>;
};
const messages = [
{ role: "user", content: "latest ask", timestamp: 1 },
{ role: "assistant", content: [{ type: "text", text: "final reply" }], timestamp: 4 },
];
const next = materializeVisibleStreamState(messages, state, {
...visibleStreamOptions,
persistCommentary: false,
});
// Keyed commentary stays live during streaming but is never materialized,
// so only the user ask and the final reply remain.
expect(next.map(messageText)).toEqual(["latest ask", "final reply"]);
});
it("still materializes the current stream tail when persistCommentary is false", () => {
const state = {
chatStream: "draft answer",
chatStreamStartedAt: 3,
chatStreamSegments: [{ text: "transient preamble", ts: 2, itemId: "preamble-1" }],
} satisfies StreamReconciliationState & {
chatStreamSegments: Array<{ text: string; ts: number; itemId: string }>;
};
const messages = [{ role: "user", content: "latest ask", timestamp: 1 }];
const next = materializeVisibleStreamState(messages, state, {
...visibleStreamOptions,
persistCommentary: false,
});
// The keyed preamble is dropped, but the non-keyed current draft is preserved.
expect(next.map(messageText)).toEqual(["latest ask", "draft answer"]);
});
it("materializes keyed commentary parts when persistCommentary is true (persist mode)", () => {
const state = {
chatStream: null,
chatStreamStartedAt: null,
chatStreamSegments: [{ text: "kept preamble", ts: 2, itemId: "preamble-1" }],
} satisfies StreamReconciliationState & {
chatStreamSegments: Array<{ text: string; ts: number; itemId: string }>;
};
const messages = [
{ role: "user", content: "latest ask", timestamp: 1 },
{ role: "assistant", content: [{ type: "text", text: "final reply" }], timestamp: 4 },
];
const next = materializeVisibleStreamState(messages, state, {
...visibleStreamOptions,
persistCommentary: true,
});
expect(next.map(messageText)).toEqual(["latest ask", "kept preamble", "final reply"]);
});
it("replaces current-stream fallbacks with matching terminal messages", () => {
const state = {
chatStream: "draft answer",
+10
View File
@@ -42,6 +42,10 @@ export type MaterializeVisibleStreamOptions = {
includeCurrent?: boolean;
requirePersistedTool?: boolean;
replacementMessages?: unknown[];
// When false, keyed commentary/preamble parts are not materialized into the
// persistent message list, so they clear with the final answer (transient mode).
// Defaults to true (persist commentary).
persistCommentary?: boolean;
isHiddenAssistantMessage: AssistantMessageVisibility;
isHiddenStreamText: StreamVisibility;
};
@@ -450,7 +454,13 @@ export function materializeVisibleStreamState(
opts: MaterializeVisibleStreamOptions,
): unknown[] {
let nextMessages = messages;
const persistCommentary = opts.persistCommentary !== false;
for (const part of visibleAssistantStreamParts(state, opts)) {
// Transient mode: keyed commentary stays live during streaming but is never
// materialized, so it disappears once the final message lands.
if (!persistCommentary && part.itemId) {
continue;
}
const replacementMessages = opts.replacementMessages ?? [];
if (
hasAssistantStreamPartReplacement(
+62
View File
@@ -583,6 +583,68 @@ describe("handleChatEvent", () => {
expect(state.chatStreamStartedAt).toBe(null);
});
it("persists keyed commentary alongside the final answer by default", () => {
const user = { role: "user", content: [{ type: "text", text: "Ask" }], timestamp: 1 };
const state = createState({
sessionKey: "main",
chatRunId: "run-1",
chatMessages: [user],
chatStream: null,
chatStreamStartedAt: null,
}) as ChatState & {
chatStreamSegments: Array<{ text: string; ts: number; itemId: string }>;
};
state.chatStreamSegments = [{ text: "Looking into it.", ts: 2, itemId: "preamble-1" }];
const payload: ChatEventPayload = {
runId: "run-1",
sessionKey: "main",
state: "final",
message: {
role: "assistant",
content: [{ type: "text", text: "Final answer." }],
timestamp: 5,
},
};
expect(handleChatEvent(state, payload)).toBe("final");
expect(state.chatMessages).toHaveLength(3);
expectTextChatMessage(state.chatMessages[0], "user", "Ask");
expectTextChatMessage(state.chatMessages[1], "assistant", "Looking into it.");
expectTextChatMessage(state.chatMessages[2], "assistant", "Final answer.");
expect(state.chatStreamSegments).toEqual([]);
});
it("clears keyed commentary with the final answer when chatPersistCommentary is false", () => {
const user = { role: "user", content: [{ type: "text", text: "Ask" }], timestamp: 1 };
const state = createState({
sessionKey: "main",
chatRunId: "run-1",
chatMessages: [user],
chatStream: null,
chatStreamStartedAt: null,
settings: { chatPersistCommentary: false },
}) as ChatState & {
chatStreamSegments: Array<{ text: string; ts: number; itemId: string }>;
};
state.chatStreamSegments = [{ text: "Looking into it.", ts: 2, itemId: "preamble-1" }];
const payload: ChatEventPayload = {
runId: "run-1",
sessionKey: "main",
state: "final",
message: {
role: "assistant",
content: [{ type: "text", text: "Final answer." }],
timestamp: 5,
},
};
expect(handleChatEvent(state, payload)).toBe("final");
expect(state.chatMessages).toHaveLength(2);
expectTextChatMessage(state.chatMessages[0], "user", "Ask");
expectTextChatMessage(state.chatMessages[1], "assistant", "Final answer.");
expect(state.chatStreamSegments).toEqual([]);
});
it("reconciles cached run and indicator state on terminal events", () => {
vi.useFakeTimers();
try {
+8
View File
@@ -204,11 +204,17 @@ function materializeVisibleAssistantStreamMessages(
): unknown[] {
return materializeVisibleStreamState(messages, state, {
...opts,
persistCommentary: chatPersistCommentaryEnabled(state),
isHiddenAssistantMessage: shouldHideAssistantChatMessage,
isHiddenStreamText: isHiddenAssistantStreamText,
});
}
function chatPersistCommentaryEnabled(state: ChatState): boolean {
// Per-viewer display preference; defaults to persist when unset (e.g. tests/headless).
return state.settings?.chatPersistCommentary !== false;
}
function hasTranscriptMeta(message: unknown): boolean {
return Boolean(
message &&
@@ -405,6 +411,8 @@ export type ChatState = {
agentsList?: ChatAgentsListSnapshot | null;
agentsSelectedId?: string | null;
hello?: GatewayHelloOk | null;
// Narrow view of UiSettings; the runtime host is the full app component.
settings?: { chatPersistCommentary?: boolean };
};
type ChatAgentsListSnapshot = Partial<Omit<AgentsListResult, "agents">> & {
+2
View File
@@ -140,6 +140,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatShowThinking: true,
chatShowToolCalls: true,
chatPersistCommentary: true,
chatAutoScroll: "near-bottom",
splitRatio: 0.6,
navCollapsed: false,
@@ -271,6 +272,7 @@ describe("loadSettings default gateway URL derivation", () => {
themeMode: "system",
chatShowThinking: true,
chatShowToolCalls: true,
chatPersistCommentary: true,
chatAutoScroll: "near-bottom",
splitRatio: 0.6,
navCollapsed: false,
+9
View File
@@ -88,6 +88,9 @@ export type UiSettings = {
themeMode: ThemeMode;
chatShowThinking: boolean;
chatShowToolCalls: boolean;
// Keep streamed commentary/preamble blocks visible after the final answer (default true).
// When false, commentary clears as the final message arrives (transient progress).
chatPersistCommentary?: boolean;
chatAutoScroll?: ChatAutoScrollMode;
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
navCollapsed: boolean; // Collapsible sidebar state
@@ -246,6 +249,7 @@ export function loadSettings(): UiSettings {
themeMode: "system",
chatShowThinking: true,
chatShowToolCalls: true,
chatPersistCommentary: true,
chatAutoScroll: "near-bottom",
splitRatio: 0.6,
navCollapsed: false,
@@ -291,6 +295,10 @@ export function loadSettings(): UiSettings {
typeof parsed.chatShowToolCalls === "boolean"
? parsed.chatShowToolCalls
: defaults.chatShowToolCalls,
chatPersistCommentary:
typeof parsed.chatPersistCommentary === "boolean"
? parsed.chatPersistCommentary
: defaults.chatPersistCommentary,
chatAutoScroll: normalizeChatAutoScrollMode(parsed.chatAutoScroll),
splitRatio:
typeof parsed.splitRatio === "number" &&
@@ -490,6 +498,7 @@ function persistSettings(next: UiSettings) {
themeMode: next.themeMode,
chatShowThinking: next.chatShowThinking,
chatShowToolCalls: next.chatShowToolCalls,
chatPersistCommentary: next.chatPersistCommentary ?? true,
chatAutoScroll: normalizeChatAutoScrollMode(next.chatAutoScroll),
splitRatio: next.splitRatio,
navCollapsed: next.navCollapsed,