mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-16 20:50:18 +00:00
feat: add transparent stream activity display
This commit is contained in:
@@ -52,6 +52,9 @@ docs/AGENTS.md
|
||||
docs/CLAUDE.md
|
||||
docs/.cursorrules
|
||||
docs/.windsurfrules
|
||||
.specify/
|
||||
AGENT_TRACE.yaml
|
||||
EXECUTION_LOG.md
|
||||
|
||||
# Local-only PR review harness: rendering drivers, sample bank, fixtures.
|
||||
# Used by Claude during deep reviews; never shared in the repo.
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Transparent Stream activity display mode.** Settings → Appearance now includes an opt-in chat activity display mode that renders each Thinking/tool event as its own chronological expandable row, with live tool completion updating the same row and settled/reloaded transcripts preserving the same event shape. Compact Worklog remains the default. (#3820)
|
||||
- **Transparent Stream turn-level collapse + old-event fading + per-turn footer.** In Transparent Stream, clicking the assistant role label (the Hermes chat name tag) collapses the entire event stack underneath into a compact "output only" view with an animated chevron. Older event rows fade progressively from medium to low opacity (steps of ~0.18, floor at 0.32) so the eye lands on the most recent activity; hover restores full opacity. A bottom-of-turn footer mirrors the live run-status line for settled turns, showing elapsed time, first-token time (TTFT), token usage, and final status. (#3820)
|
||||
|
||||
## [v0.51.359] — 2026-06-11 — Release LW (assistant turn anchor phase 0 scaffold)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -5746,6 +5746,7 @@ _SETTINGS_DEFAULTS = {
|
||||
"font_size": "default", # small | default | large | xlarge
|
||||
"session_jump_buttons": False, # show Start/End transcript jump pills
|
||||
"session_endless_scroll": False, # auto-load older transcript pages while scrolling upward
|
||||
"chat_activity_display_mode": "compact_worklog", # compact_worklog | transparent_stream
|
||||
"worklog_details_expanded_default": False, # opt-in: expand Worklog details by default; default remains folded
|
||||
"pinned_sessions_limit": 3, # maximum active pinned sessions shown in the sidebar
|
||||
"inflight_state_max_sessions": 8, # max active-stream recovery snapshots kept in browser localStorage
|
||||
@@ -5900,6 +5901,7 @@ _SETTINGS_ENUM_VALUES = {
|
||||
"font_size": {"small", "default", "large", "xlarge"},
|
||||
"auto_title_refresh_every": {"0", "5", "10", "20"},
|
||||
"busy_input_mode": {"queue", "interrupt", "steer"},
|
||||
"chat_activity_display_mode": {"compact_worklog", "transparent_stream"},
|
||||
}
|
||||
_SETTINGS_INT_RANGES = {
|
||||
"pinned_sessions_limit": (1, 99),
|
||||
|
||||
@@ -1798,6 +1798,8 @@ function applyBotName(){
|
||||
window._whatsNewSummaryEnabled=!!s.whats_new_summary_enabled;
|
||||
window._showThinking=s.show_thinking!==false;
|
||||
window._simplifiedToolCalling=true;
|
||||
window._chatActivityDisplayMode=s.chat_activity_display_mode==='transparent_stream'?'transparent_stream':'compact_worklog';
|
||||
window._transparentStream=window._chatActivityDisplayMode==='transparent_stream';
|
||||
window._terminalAutoExpandOnOutput=!!s.terminal_auto_expand_on_output;
|
||||
window._worklogDetailsExpandedByDefault=!!(
|
||||
Object.prototype.hasOwnProperty.call(s,'worklog_details_expanded_default')
|
||||
@@ -1903,6 +1905,8 @@ function applyBotName(){
|
||||
window._whatsNewSummaryEnabled=false;
|
||||
window._showThinking=true;
|
||||
window._simplifiedToolCalling=true;
|
||||
window._chatActivityDisplayMode='compact_worklog';
|
||||
window._transparentStream=false;
|
||||
window._terminalAutoExpandOnOutput=false;
|
||||
window._sessionJumpButtonsEnabled=false;
|
||||
window._sidebarDensity='compact';
|
||||
|
||||
@@ -560,6 +560,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Sidebar tabs',
|
||||
settings_desc_tab_visibility: 'Choose which tabs appear in the sidebar and rail. Drag chips to reorder them. Chat and Settings are always visible.',
|
||||
@@ -1970,6 +1976,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Se abilitato, i messaggi precedenti si caricano automaticamente scorrendo in alto. Se disabilitato, usa il pulsante messaggi precedenti.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Schede della barra laterale',
|
||||
settings_desc_tab_visibility: 'Scegli quali schede mostrare nella barra laterale e nel rail. Chat e Impostazioni sono sempre visibili.',
|
||||
@@ -3371,6 +3383,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: '有効にすると、上にスクロールしたとき古いメッセージを自動で読み込みます。無効の場合は古いメッセージボタンを使います。',
|
||||
settings_label_worklog_details_expanded_default: 'Worklog の詳細を自動的に開く',
|
||||
settings_desc_worklog_details_expanded_default: '有効にすると、新しい Worklog の詳細が展開された状態で始まり、ツール、Thinking、進捗カードをクリックなしで確認できます。無効の場合、Worklog の詳細はデフォルトで折りたたまれます。ターンごとの手動折りたたみ/展開は優先されます。',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'サイドバータブ',
|
||||
settings_desc_tab_visibility: 'サイドバーとレールに表示するタブを選択します。チャットと設定は常に表示されます。',
|
||||
@@ -5391,6 +5409,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Если включено, старые сообщения загружаются автоматически при прокрутке вверх. Если выключено, используйте кнопку загрузки старых сообщений.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Вкладки боковой панели',
|
||||
settings_desc_tab_visibility: 'Выберите, какие вкладки отображаются на боковой панели и в рейле. Чат и настройки всегда видны.',
|
||||
@@ -6717,6 +6741,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Si está activado, los mensajes antiguos se cargan automáticamente al desplazarte hacia arriba. Si está desactivado, usa el botón de mensajes antiguos.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Pestañas de la barra lateral',
|
||||
settings_desc_tab_visibility: 'Elige qué pestañas aparecen en la barra lateral y el rail. Chat y Configuración siempre están visibles.',
|
||||
@@ -7743,6 +7773,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Wenn aktiviert, werden ältere Nachrichten beim Hochscrollen automatisch geladen. Wenn deaktiviert, nutzt du den Button für ältere Nachrichten.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Seitenleiste-Tabs',
|
||||
settings_desc_tab_visibility: 'Wähle, welche Tabs in der Seitenleiste und im Rail angezeigt werden. Ziehe die Chips, um die Reihenfolge zu ändern. Chat und Einstellungen sind immer sichtbar.',
|
||||
@@ -9404,6 +9440,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: '启用后,向上滚动时会自动加载更早的消息。禁用时请使用加载更早消息按钮。',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: '侧边栏标签',
|
||||
settings_desc_tab_visibility: '选择在侧边栏和导航栏中显示哪些标签。聊天和设置始终可见。',
|
||||
@@ -10125,6 +10167,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: '啟用後,向上捲動時會自動載入較早訊息。停用時請使用載入較早訊息按鈕。',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: '側邊欄標籤',
|
||||
settings_desc_tab_visibility: '選擇在側邊欄和導覽列中顯示哪些標籤。聊天和設定始終可見。',
|
||||
@@ -11416,6 +11464,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Quando ativado, mensagens antigas carregam automaticamente ao rolar para cima. Quando desativado, use o botão de mensagens antigas.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Abas da barra lateral',
|
||||
settings_desc_tab_visibility: 'Escolha quais abas aparecem na barra lateral e no rail. Chat e Configurações estão sempre visíveis.',
|
||||
@@ -12718,6 +12772,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: '활성화하면 위로 스크롤할 때 이전 메시지를 자동으로 불러옵니다. 비활성화하면 이전 메시지 버튼을 사용합니다.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: '사이드바 탭',
|
||||
settings_desc_tab_visibility: '사이드바와 레일에 표시할 탭을 선택하세요. 채팅과 설정은 항상 표시됩니다.',
|
||||
@@ -14037,6 +14097,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Lorsqu\'ils sont activés, les anciens messages se chargent automatiquement lorsque vous faites défiler vers le haut. Lorsqu\'il est désactivé, utilisez le bouton des messages plus anciens.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Onglets de la barre latérale',
|
||||
settings_desc_tab_visibility: 'Choisissez quels onglets apparaissent dans la barre latérale et le rail. Chat et Paramètres sont toujours visibles.',
|
||||
@@ -15457,6 +15523,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Etkinleştirildiğinde, yukarı doğru kaydırdığınızda eski mesajlar otomatik olarak yüklenir. Devre dışı bırakıldığında eski mesajlar düğmesini kullanın.',
|
||||
settings_label_worklog_details_expanded_default: 'Open Worklog details automatically',
|
||||
settings_desc_worklog_details_expanded_default: 'When enabled, new Worklog details start expanded so tool, thinking, and progress cards are visible without an extra click. When off, Worklog details stay folded by default; per-turn manual collapse/expand choices still win.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Kenar çubuğu sekmeleri',
|
||||
settings_desc_tab_visibility: 'Kenar çubuğunda ve rayda hangi sekmelerin görüneceğini seçin. Sohbet ve Ayarlar her zaman görünür durumdadır.',
|
||||
@@ -16869,6 +16941,12 @@ const LOCALES = {
|
||||
settings_desc_session_endless_scroll: 'Gdy ta opcja jest włączona, starsze wiadomości ładują się automatycznie przy przewijaniu w górę. Gdy jest wyłączona, użyj przycisku wczytywania starszych wiadomości.',
|
||||
settings_label_worklog_details_expanded_default: 'Otwieraj szczegóły Worklog automatycznie',
|
||||
settings_desc_worklog_details_expanded_default: 'Gdy ta opcja jest włączona, nowe szczegóły Worklog zaczynają rozwinięte, dzięki czemu karty narzędzi, Thinking i postępu są widoczne bez dodatkowego kliknięcia. Gdy jest wyłączona, szczegóły Worklog pozostają domyślnie zwinięte. Ręczne decyzje o zwinięciu/rozwinięciu w danej turze mają pierwszeństwo.',
|
||||
settings_label_chat_activity_display_mode: 'Activity display',
|
||||
settings_desc_chat_activity_display_mode: 'Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.',
|
||||
first_token_time: 'Time to first token',
|
||||
done: 'Done',
|
||||
settings_option_compact_worklog: 'Compact Worklog',
|
||||
settings_option_transparent_stream: 'Transparent Stream',
|
||||
|
||||
settings_label_tab_visibility: 'Karty paska bocznego',
|
||||
settings_desc_tab_visibility: 'Wybierz, które karty pojawiają się na pasku bocznym i szynie. Przeciągnij elementy, aby zmienić ich kolejność. Czat i Ustawienia są zawsze widoczne.',
|
||||
|
||||
@@ -44,6 +44,7 @@ const LI_PATHS = {
|
||||
'globe': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
|
||||
'play': '<polygon points="5 3 19 12 5 21 5 3"/>',
|
||||
'wrench': '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
||||
'plug': '<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a6 6 0 0 1-12 0V8Z"/>',
|
||||
'brain': '<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/>',
|
||||
'sparkles': '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
|
||||
'book-open': '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>',
|
||||
|
||||
@@ -1004,6 +1004,22 @@
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_session_endless_scroll">When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.</div>
|
||||
</div>
|
||||
<div class="settings-field" style="margin-top:8px">
|
||||
<label id="settingsChatActivityDisplayModeLabel" for="settingsChatActivityDisplayMode" data-i18n="settings_label_chat_activity_display_mode">Activity display</label>
|
||||
<div class="chat-activity-mode-toggle" role="group" aria-labelledby="settingsChatActivityDisplayModeLabel">
|
||||
<button type="button" class="chat-activity-mode-btn active" data-chat-activity-mode="compact_worklog" onclick="_pickChatActivityDisplayMode('compact_worklog')">
|
||||
<span data-i18n="settings_option_compact_worklog">Compact Worklog</span>
|
||||
</button>
|
||||
<button type="button" class="chat-activity-mode-btn" data-chat-activity-mode="transparent_stream" onclick="_pickChatActivityDisplayMode('transparent_stream')">
|
||||
<span data-i18n="settings_option_transparent_stream">Transparent Stream</span>
|
||||
</button>
|
||||
</div>
|
||||
<select id="settingsChatActivityDisplayMode" style="display:none">
|
||||
<option value="compact_worklog" data-i18n="settings_option_compact_worklog">Compact Worklog</option>
|
||||
<option value="transparent_stream" data-i18n="settings_option_transparent_stream">Transparent Stream</option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_chat_activity_display_mode">Compact Worklog groups supporting activity into a quiet summary. Transparent Stream keeps each thinking/tool event as its own chronological expandable row.</div>
|
||||
</div>
|
||||
<div class="settings-field" style="margin-top:8px">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsWorklogDetailsExpandedDefault" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
|
||||
+1
-2
@@ -1891,7 +1891,6 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const _STREAM_FADE_STAGGER_MS=16;
|
||||
const _STREAM_FADE_DONE_MAX_MS=320;
|
||||
const _STREAM_FADE_DONE_DRAIN_MAX_MS=900;
|
||||
const _streamFadeEnabledForStream=window._fadeTextEffect===true;
|
||||
|
||||
function _mergeSettledToolCallsWithLiveMetadata(rawCalls){
|
||||
const liveCalls=Array.isArray(S.toolCalls)?S.toolCalls:[];
|
||||
@@ -2116,7 +2115,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
_renderPending=false;
|
||||
}
|
||||
function _shouldUseStreamFade(){
|
||||
return _streamFadeEnabledForStream;
|
||||
return window._fadeTextEffect===true;
|
||||
}
|
||||
function _streamFadeSkipNode(node){
|
||||
if(!node||node.nodeType!==1) return false;
|
||||
|
||||
+45
-1
@@ -6204,10 +6204,12 @@ function _applyTtsEnabled(enabled){
|
||||
|
||||
function _appearancePayloadFromUi(){
|
||||
const worklogDetailsExpanded=!!($('settingsWorklogDetailsExpandedDefault')||{}).checked;
|
||||
const chatActivityModeSel=$('settingsChatActivityDisplayMode');
|
||||
return {
|
||||
theme: ($('settingsTheme')||{}).value || localStorage.getItem('hermes-theme') || 'dark',
|
||||
skin: ($('settingsSkin')||{}).value || localStorage.getItem('hermes-skin') || 'default',
|
||||
font_size: ($('settingsFontSize')||{}).value || localStorage.getItem('hermes-font-size') || 'default',
|
||||
chat_activity_display_mode: chatActivityModeSel&&chatActivityModeSel.value==='transparent_stream'?'transparent_stream':'compact_worklog',
|
||||
session_jump_buttons: !!($('settingsSessionJumpButtons')||{}).checked,
|
||||
session_endless_scroll: !!($('settingsSessionEndlessScroll')||{}).checked,
|
||||
worklog_details_expanded_default: worklogDetailsExpanded,
|
||||
@@ -6217,6 +6219,27 @@ function _appearancePayloadFromUi(){
|
||||
};
|
||||
}
|
||||
|
||||
function _syncChatActivityDisplayModeControl(mode){
|
||||
const next=mode==='transparent_stream'?'transparent_stream':'compact_worklog';
|
||||
const select=$('settingsChatActivityDisplayMode');
|
||||
if(select) select.value=next;
|
||||
document.querySelectorAll('[data-chat-activity-mode]').forEach(btn=>{
|
||||
const active=btn.getAttribute('data-chat-activity-mode')===next;
|
||||
btn.classList.toggle('active',active);
|
||||
btn.setAttribute('aria-pressed',active?'true':'false');
|
||||
});
|
||||
window._chatActivityDisplayMode=next;
|
||||
window._transparentStream=next==='transparent_stream';
|
||||
}
|
||||
|
||||
function _pickChatActivityDisplayMode(mode){
|
||||
_syncChatActivityDisplayModeControl(mode);
|
||||
if(typeof clearMessageRenderCache==='function') clearMessageRenderCache();
|
||||
if(typeof renderMessages==='function') renderMessages({preserveScroll:true});
|
||||
_scheduleAppearanceAutosave();
|
||||
}
|
||||
if(typeof window!=='undefined') window._pickChatActivityDisplayMode=_pickChatActivityDisplayMode;
|
||||
|
||||
function _setAppearanceAutosaveStatus(state){
|
||||
const el=$('settingsAppearanceAutosaveStatus');
|
||||
if(!el) return;
|
||||
@@ -6263,6 +6286,11 @@ async function _autosaveAppearanceSettings(payload){
|
||||
}
|
||||
if(saved){
|
||||
window._sessionJumpButtonsEnabled=!!saved.session_jump_buttons;
|
||||
if(Object.prototype.hasOwnProperty.call(saved,'chat_activity_display_mode')){
|
||||
_syncChatActivityDisplayModeControl(saved.chat_activity_display_mode);
|
||||
if(typeof clearMessageRenderCache==='function') clearMessageRenderCache();
|
||||
if(typeof renderMessages==='function') renderMessages({preserveScroll:true});
|
||||
}
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
}
|
||||
window._sessionEndlessScrollEnabled=!!(saved&&saved.session_endless_scroll);
|
||||
@@ -6501,6 +6529,13 @@ async function loadSettingsPanel(){
|
||||
};
|
||||
}
|
||||
const worklogDetailsExpandedCb=$('settingsWorklogDetailsExpandedDefault');
|
||||
const chatActivityModeSel=$('settingsChatActivityDisplayMode');
|
||||
if(chatActivityModeSel){
|
||||
_syncChatActivityDisplayModeControl(settings.chat_activity_display_mode);
|
||||
chatActivityModeSel.addEventListener('change',()=>{
|
||||
_pickChatActivityDisplayMode(chatActivityModeSel.value);
|
||||
},{once:false});
|
||||
}
|
||||
if(worklogDetailsExpandedCb){
|
||||
const worklogDetailsExpanded=Object.prototype.hasOwnProperty.call(settings,'worklog_details_expanded_default')
|
||||
? settings.worklog_details_expanded_default
|
||||
@@ -6648,7 +6683,14 @@ async function loadSettingsPanel(){
|
||||
pinnedLimitField.addEventListener('input',()=>{window._pinnedSessionsLimit=parseInt(pinnedLimitField.value,10)||3;_schedulePreferencesAutosave();},{once:false});
|
||||
}
|
||||
const fadeTextCb=$('settingsFadeTextEffect');
|
||||
if(fadeTextCb){fadeTextCb.checked=!!settings.fade_text_effect;window._fadeTextEffect=fadeTextCb.checked;fadeTextCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
|
||||
if(fadeTextCb){
|
||||
fadeTextCb.checked=!!settings.fade_text_effect;
|
||||
window._fadeTextEffect=fadeTextCb.checked;
|
||||
fadeTextCb.addEventListener('change',()=>{
|
||||
window._fadeTextEffect=fadeTextCb.checked;
|
||||
_schedulePreferencesAutosave();
|
||||
},{once:false});
|
||||
}
|
||||
const terminalAutoExpandCb=$('settingsTerminalAutoExpand');
|
||||
if(terminalAutoExpandCb){terminalAutoExpandCb.checked=!!settings.terminal_auto_expand_on_output;window._terminalAutoExpandOnOutput=terminalAutoExpandCb.checked;terminalAutoExpandCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
|
||||
const apiRedactCb=$('settingsApiRedact');
|
||||
@@ -7763,6 +7805,7 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
window._whatsNewSummaryEnabled=!!body.whats_new_summary_enabled;
|
||||
window._showThinking=body.show_thinking!==false;
|
||||
window._simplifiedToolCalling=true;
|
||||
_syncChatActivityDisplayModeControl(body.chat_activity_display_mode);
|
||||
window._terminalAutoExpandOnOutput=!!body.terminal_auto_expand_on_output;
|
||||
window._sessionJumpButtonsEnabled=!!body.session_jump_buttons;
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
@@ -8100,6 +8143,7 @@ async function saveSettings(andClose){
|
||||
body.font_size=fontSize;
|
||||
body.session_jump_buttons=!!($('settingsSessionJumpButtons')||{}).checked;
|
||||
body.session_endless_scroll=!!($('settingsSessionEndlessScroll')||{}).checked;
|
||||
body.chat_activity_display_mode=(($('settingsChatActivityDisplayMode')||{}).value==='transparent_stream')?'transparent_stream':'compact_worklog';
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_quota_chip=showQuotaChip===true;
|
||||
|
||||
@@ -3136,6 +3136,297 @@ body.resizing .sidebar{transition:none!important;}
|
||||
.tool-arg-val{color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;font-size:var(--message-code-font-size);white-space:pre-wrap;word-break:break-word;display:block;overflow-x:auto;}
|
||||
.tool-card-result pre{font-size:var(--message-code-font-size);color:var(--muted);font-family:'SF Mono',ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;margin:0;line-height:1.55;}
|
||||
|
||||
/* ── Transparent Stream event rows ── */
|
||||
/* Box rhythm: each event is a compact rounded card with a thin, transparent
|
||||
border, a 2px type rail, and tight spacing. Keep the newest activity easy
|
||||
to scan without letting the execution trace overpower the final answer. */
|
||||
.transparent-event-controls{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
margin:1px 0 3px var(--msg-rail);
|
||||
max-width:var(--msg-max);
|
||||
color:var(--muted);
|
||||
font-size:11px;
|
||||
line-height:1.3;
|
||||
}
|
||||
.transparent-event-controls-label{font-weight:600;color:var(--muted);}
|
||||
.transparent-event-action,
|
||||
.transparent-event-control{
|
||||
cursor:pointer;
|
||||
color:var(--muted);
|
||||
opacity:.78;
|
||||
text-decoration:underline;
|
||||
text-decoration-color:transparent;
|
||||
text-underline-offset:3px;
|
||||
}
|
||||
.transparent-event-action:hover,
|
||||
.transparent-event-control:hover{color:var(--text);opacity:1;text-decoration-color:currentColor;}
|
||||
.transparent-event-row{
|
||||
margin:1px 0 1px var(--msg-rail);
|
||||
max-width:var(--msg-max);
|
||||
width:calc(100% - var(--msg-rail));
|
||||
border:1px solid color-mix(in srgb,var(--accent) 12%,var(--border-subtle));
|
||||
border-left:2px solid color-mix(in srgb,var(--accent) 42%,transparent);
|
||||
border-radius:12px;
|
||||
background:linear-gradient(90deg,color-mix(in srgb,var(--accent-bg) 22%,transparent),color-mix(in srgb,var(--surface) 88%,transparent) 34%);
|
||||
overflow:hidden;
|
||||
box-shadow:0 1px 0 rgba(0,0,0,.03);
|
||||
animation:transparent-event-enter .18s ease-out both;
|
||||
transition:border-color .22s ease,box-shadow .22s ease,opacity .22s ease;
|
||||
}
|
||||
.transparent-event-row[data-event-type="thinking"]{
|
||||
background:linear-gradient(90deg,color-mix(in srgb,var(--accent-bg-strong) 30%,transparent),color-mix(in srgb,var(--accent-bg) 28%,color-mix(in srgb,var(--surface) 88%,transparent)) 42%);
|
||||
border-color:color-mix(in srgb,var(--accent) 18%,var(--border-subtle));
|
||||
border-left-color:color-mix(in srgb,var(--accent) 58%,transparent);
|
||||
}
|
||||
.transparent-event-row[data-expanded="1"]{
|
||||
border-color:color-mix(in srgb,var(--accent) 28%,var(--border-subtle));
|
||||
box-shadow:0 4px 14px rgba(0,0,0,.06);
|
||||
}
|
||||
.transparent-event-row:hover{border-color:color-mix(in srgb,var(--accent) 38%,var(--border-subtle));}
|
||||
.transparent-event-row .tool-card{
|
||||
border:0;
|
||||
border-radius:0;
|
||||
background:transparent;
|
||||
overflow:visible;
|
||||
}
|
||||
.transparent-event-row .tool-card-header,
|
||||
.transparent-event-row .thinking-card-header{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:5px;
|
||||
min-height:20px;
|
||||
padding:2px 4px 2px 8px;
|
||||
margin-left:0;
|
||||
border-radius:0;
|
||||
color:var(--muted);
|
||||
transition:background .18s ease,color .18s ease;
|
||||
}
|
||||
.transparent-event-row .tool-card-header:hover,
|
||||
.transparent-event-row .thinking-card-header:hover{background:var(--hover-bg);color:var(--text);}
|
||||
.transparent-event-row .tool-card-icon,
|
||||
.transparent-event-row .thinking-card-icon{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:14px;height:14px;
|
||||
flex-shrink:0;
|
||||
order:1;
|
||||
opacity:.85;
|
||||
}
|
||||
.transparent-event-row .tool-card-icon svg,
|
||||
.transparent-event-row .thinking-card-icon svg{width:11px;height:11px;}
|
||||
.transparent-event-row .tool-card-name{
|
||||
font-weight:600;
|
||||
color:var(--text);
|
||||
opacity:.78;
|
||||
font-size:11.5px;
|
||||
order:2;
|
||||
flex-shrink:0;
|
||||
max-width:40%;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.transparent-event-row .tool-card-title,
|
||||
.transparent-event-row .tool-card-preview,
|
||||
.transparent-event-row .thinking-card-label,
|
||||
.transparent-event-thinking-preview{
|
||||
display:inline;
|
||||
min-width:0;
|
||||
flex:1 1 auto;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
color:var(--muted);
|
||||
opacity:.74;
|
||||
font-size:11.5px;
|
||||
order:3;
|
||||
}
|
||||
.transparent-event-status{
|
||||
flex-shrink:0;
|
||||
order:4;
|
||||
color:var(--muted);
|
||||
opacity:.7;
|
||||
font-size:10px;
|
||||
font-weight:500;
|
||||
letter-spacing:.02em;
|
||||
padding:1px 5px;
|
||||
border-radius:6px;
|
||||
background:color-mix(in srgb,var(--accent) 8%,transparent);
|
||||
}
|
||||
.transparent-event-copy{
|
||||
border:0;
|
||||
background:transparent;
|
||||
color:var(--muted);
|
||||
opacity:.5;
|
||||
padding:1px 2px;
|
||||
cursor:pointer;
|
||||
flex-shrink:0;
|
||||
order:9;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
border-radius:4px;
|
||||
transition:opacity .18s ease,color .18s ease,background .18s ease;
|
||||
}
|
||||
.transparent-event-copy:hover{opacity:1;color:var(--text);background:color-mix(in srgb,var(--hover-bg) 60%,transparent);}
|
||||
.transparent-event-row .tool-card-toggle,
|
||||
.transparent-event-row .thinking-card-toggle{
|
||||
order:10;
|
||||
flex-shrink:0;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:14px;height:14px;
|
||||
opacity:.55;
|
||||
transition:opacity .18s ease,transform .22s ease;
|
||||
}
|
||||
.transparent-event-row .tool-card:hover .tool-card-toggle,
|
||||
.transparent-event-row .thinking-card:hover .thinking-card-toggle{opacity:1;}
|
||||
.transparent-event-row .tool-card.open .tool-card-toggle,
|
||||
.transparent-event-row .thinking-card.open .thinking-card-toggle{transform:rotate(90deg);opacity:.9;}
|
||||
.transparent-event-progress{
|
||||
position:relative;
|
||||
height:3px;
|
||||
background:linear-gradient(180deg,color-mix(in srgb,var(--border-subtle) 100%,transparent) 0,color-mix(in srgb,var(--border-subtle) 40%,transparent) 100%);
|
||||
border-radius:0 0 12px 12px;
|
||||
overflow:hidden;
|
||||
margin:0 -1px -1px -1px;
|
||||
}
|
||||
.transparent-event-progress::before{
|
||||
content:"";
|
||||
position:absolute;left:0;top:0;bottom:0;
|
||||
width:0%;
|
||||
background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 70%,transparent),color-mix(in srgb,var(--accent) 95%,transparent) 50%,color-mix(in srgb,var(--accent) 70%,transparent) 100%);
|
||||
box-shadow:0 0 4px color-mix(in srgb,var(--accent) 60%,transparent);
|
||||
border-radius:inherit;
|
||||
transition:width .35s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
.transparent-event-progress[data-progress-running="1"]::before{animation:transparent-progress-shimmer 1.6s ease-in-out infinite;}
|
||||
.transparent-event-progress[data-progress-percent]::before{width:var(--transparent-progress-percent,0%);}
|
||||
.transparent-event-progress::after{
|
||||
content:"";
|
||||
position:absolute;left:0;right:0;top:0;height:1px;
|
||||
background:linear-gradient(90deg,transparent 0,color-mix(in srgb,var(--surface) 70%,transparent) 20%,color-mix(in srgb,var(--surface) 90%,transparent) 50%,color-mix(in srgb,var(--surface) 70%,transparent) 80%,transparent 100%);
|
||||
pointer-events:none;
|
||||
}
|
||||
@keyframes transparent-progress-shimmer{
|
||||
0%{left:0;right:100%;}
|
||||
100%{left:100%;right:0;}
|
||||
}
|
||||
.transparent-event-row + .transparent-event-row::before{
|
||||
content:"";
|
||||
display:block;
|
||||
height:1px;
|
||||
margin:0 12px;
|
||||
background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 18%,transparent) 20%,color-mix(in srgb,var(--accent) 28%,transparent) 50%,color-mix(in srgb,var(--accent) 18%,transparent) 80%,transparent);
|
||||
border-radius:1px;
|
||||
}
|
||||
.transparent-detail-modes{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
padding:4px 8px 0;
|
||||
color:var(--muted);
|
||||
font-size:10.5px;
|
||||
}
|
||||
.transparent-detail-mode{cursor:pointer;opacity:.72;transition:opacity .18s ease,color .18s ease;}
|
||||
.transparent-detail-mode.active{color:var(--text);opacity:1;font-weight:600;}
|
||||
.transparent-event-row .tool-card-detail,
|
||||
.transparent-event-row .thinking-card-body{
|
||||
display:block;
|
||||
max-height:0;
|
||||
opacity:0;
|
||||
overflow:hidden;
|
||||
padding:0;
|
||||
margin:0;
|
||||
border-top:1px solid transparent;
|
||||
background:transparent;
|
||||
transform:translateY(-1px);
|
||||
transition:max-height .28s cubic-bezier(.4,0,.2,1),opacity .2s ease,transform .24s ease,border-top-color .2s ease,padding .22s ease;
|
||||
}
|
||||
.transparent-event-row .tool-card.open .tool-card-detail,
|
||||
.transparent-event-row .thinking-card.open .thinking-card-body{
|
||||
max-height:520px;
|
||||
opacity:1;
|
||||
transform:translateY(0);
|
||||
border-top-color:var(--border-subtle);
|
||||
padding:2px 0 4px;
|
||||
overflow:auto;
|
||||
}
|
||||
.transparent-event-row .tool-card-detail > *,
|
||||
.transparent-event-row .thinking-card-body > *{min-height:0;}
|
||||
.transparent-event-row .tool-card-args,
|
||||
.transparent-event-row .tool-card-result,
|
||||
.transparent-event-row .thinking-card-body pre{padding:6px 8px;}
|
||||
.transparent-event-row .tool-card-detail[data-transparent-detail-mode="output"] .tool-card-args{display:none;}
|
||||
.transparent-event-row .tool-card-args{margin-bottom:0;}
|
||||
.transparent-event-row .tool-arg-pair{display:flex;flex-direction:column;gap:1px;padding:3px 0;border-bottom:1px solid color-mix(in srgb,var(--border-subtle) 72%,transparent);}
|
||||
.transparent-event-row .tool-arg-pair:last-child{border-bottom:0;}
|
||||
.transparent-event-row .tool-arg-key{color:var(--muted);font-size:10px;font-family:var(--font-mono);display:block;margin-bottom:0;letter-spacing:.01em;}
|
||||
.transparent-event-row .tool-arg-val{font-family:var(--font-mono);font-size:var(--message-code-font-size);color:var(--text);line-height:1.5;}
|
||||
.transparent-event-row .tool-card-result pre,
|
||||
.transparent-event-row .thinking-card-body pre{
|
||||
background:var(--code-bg);
|
||||
border:1px solid var(--border-subtle);
|
||||
border-radius:8px;
|
||||
padding:6px 8px;
|
||||
color:var(--muted);
|
||||
line-height:1.5;
|
||||
}
|
||||
@media(max-width:600px){
|
||||
.transparent-event-controls,
|
||||
.transparent-event-row{margin-left:0;width:100%;max-width:none;}
|
||||
}
|
||||
@keyframes transparent-event-enter{
|
||||
from{opacity:0;transform:translateY(2px);}
|
||||
to{opacity:1;transform:translateY(0);}
|
||||
}
|
||||
|
||||
/* ── Old-event fading (transparent mode: medium → low) ─────────────── */
|
||||
.transparent-event-row[data-transparent-fade="1"]{opacity:.82;}
|
||||
.transparent-event-row[data-transparent-fade="2"]{opacity:.66;}
|
||||
.transparent-event-row[data-transparent-fade="3"]{opacity:.52;}
|
||||
.transparent-event-row[data-transparent-fade="4"]{opacity:.4;}
|
||||
.transparent-event-row[data-transparent-fade="5"]{opacity:.32;}
|
||||
.transparent-event-row[data-transparent-fade]{transition:opacity .22s ease;}
|
||||
.transparent-event-row[data-transparent-fade]:hover{opacity:1;}
|
||||
|
||||
/* ── Collapsible Hermes chat name tag ───────── */
|
||||
.assistant-turn[data-transparent-turn-collapsed="1"] .transparent-event-row,
|
||||
.assistant-turn[data-transparent-turn-collapsed="1"] .transparent-event-controls{
|
||||
display:none;
|
||||
}
|
||||
.assistant-turn .msg-role.assistant{cursor:pointer;user-select:none;}
|
||||
.assistant-turn .msg-role.assistant .transparent-turn-chevron{
|
||||
display:inline-block;margin-left:6px;opacity:.6;
|
||||
transition:transform .2s ease;
|
||||
}
|
||||
.assistant-turn[data-transparent-turn-collapsed="1"] .msg-role.assistant .transparent-turn-chevron{
|
||||
transform:rotate(-90deg);
|
||||
}
|
||||
|
||||
/* ── Turn footer (transparent mode: elapsed · tokens · TTFT · status) ─ */
|
||||
/* Mirrors the live run-status line for settled turns in transparent
|
||||
mode. Shows duration, first-token time, token usage, and final
|
||||
status. Lives OUTSIDE assistant-turn-blocks so it stays visible even
|
||||
when the event rows are collapsed. Tight offset and small font to
|
||||
keep the visual rhythm with the event cards above it. */
|
||||
.transparent-turn-footer{
|
||||
display:flex;align-items:center;flex-wrap:wrap;gap:6px;
|
||||
margin:2px 0 2px var(--msg-rail);
|
||||
max-width:var(--msg-max);
|
||||
font-size:11.5px;color:var(--muted);line-height:1.3;
|
||||
font-variant-numeric:tabular-nums;
|
||||
padding:2px 4px;
|
||||
}
|
||||
.transparent-turn-footer .lf-time{color:var(--text);}
|
||||
.transparent-turn-footer .lf-sep{opacity:.4;}
|
||||
.transparent-turn-footer .lf-status{color:var(--muted);opacity:.85;}
|
||||
.transparent-turn-footer .lf-ttft{color:var(--accent);opacity:.9;}
|
||||
.transparent-turn-footer .lf-tokens{font-variant-numeric:tabular-nums;}
|
||||
|
||||
/* ── Manual compression cards (transient transcript-local feedback) ── */
|
||||
.live-compression-cards{
|
||||
display:none;
|
||||
@@ -3799,6 +4090,10 @@ main.main > #mainPlugin{display:none;}
|
||||
#mainSettings .theme-pick-btn.active,
|
||||
#mainSettings .skin-pick-btn.active,
|
||||
#mainSettings .font-size-pick-btn.active{border-color:var(--accent)!important;box-shadow:0 0 0 1px var(--accent-bg-strong)!important;background:var(--surface)!important;}
|
||||
#mainSettings .chat-activity-mode-toggle{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-top:4px;}
|
||||
#mainSettings .chat-activity-mode-btn{min-height:34px;padding:7px 10px;border:1px solid var(--border2);border-radius:8px;background:var(--surface);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;}
|
||||
#mainSettings .chat-activity-mode-btn:hover{border-color:var(--accent-bg-strong);color:var(--text);background:var(--hover-bg);}
|
||||
#mainSettings .chat-activity-mode-btn.active{border-color:var(--accent);color:var(--accent-text);background:var(--accent-bg);box-shadow:0 0 0 1px var(--accent-bg-strong);}
|
||||
|
||||
/* Responsive: tighten canvas on small screens. */
|
||||
@media (max-width: 768px){
|
||||
@@ -3809,6 +4104,7 @@ main.main > #mainPlugin{display:none;}
|
||||
.settings-version-badge{white-space:nowrap;}
|
||||
.hermes-action-grid{grid-template-columns:1fr;}
|
||||
#mainSettings .settings-field{padding:14px;}
|
||||
#mainSettings .chat-activity-mode-toggle{grid-template-columns:1fr;}
|
||||
}
|
||||
|
||||
/* ── Provider cards (Settings → Providers) ──
|
||||
|
||||
+766
-55
@@ -2655,6 +2655,12 @@ function _formatTurnDuration(seconds){
|
||||
if(h)return`${h}h ${m}m`;
|
||||
return`${m}m ${s}s`;
|
||||
}
|
||||
function _formatFirstToken(ms){
|
||||
const n=Number(ms);
|
||||
if(!Number.isFinite(n)||n<0)return'';
|
||||
if(n<1000)return`${Math.round(n)}ms`;
|
||||
return`${(n/1000).toFixed(2)}s`;
|
||||
}
|
||||
function _formatActiveElapsedTimer(seconds){
|
||||
const n=Number(seconds);
|
||||
if(!Number.isFinite(n)||n<0)return'';
|
||||
@@ -6562,6 +6568,499 @@ function _thinkingActivityNode(text, open){
|
||||
_renderThinkingInto(row,text);
|
||||
return row;
|
||||
}
|
||||
function chatActivityMode(){
|
||||
if(typeof window==='undefined') return 'compact_worklog';
|
||||
return window._chatActivityDisplayMode || (window._transparentStream ? 'transparent_stream' : 'compact_worklog') || 'compact_worklog';
|
||||
}
|
||||
function isTransparentStream(){
|
||||
return chatActivityMode()==='transparent_stream';
|
||||
}
|
||||
function isCompactWorklogMode(){
|
||||
return isSimplifiedToolCalling()&&!isTransparentStream();
|
||||
}
|
||||
if(typeof window!=='undefined'){
|
||||
window.chatActivityMode=chatActivityMode;
|
||||
window.isTransparentStream=isTransparentStream;
|
||||
window.isCompactWorklogMode=isCompactWorklogMode;
|
||||
}
|
||||
function _toolShortName(name){
|
||||
const raw=String(name||'').trim();
|
||||
if(!raw) return 'tool';
|
||||
if(raw.startsWith('mcp__')){
|
||||
const parts=raw.split('__').filter(Boolean);
|
||||
if(parts.length>1) return parts.slice(1).join('/');
|
||||
}
|
||||
if(raw.startsWith('mcp.')){
|
||||
const parts=raw.split('.').filter(Boolean);
|
||||
if(parts.length>1) return parts.slice(1).join('/');
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
function _transparentEventPreview(text){
|
||||
const clean=_sanitizeThinkingDisplayText(String(text||'')).replace(/\s+/g,' ').trim();
|
||||
if(!clean) return '';
|
||||
return clean.length>180?`${clean.slice(0,177)}...`:clean;
|
||||
}
|
||||
function _transparentToolStatus(tc){
|
||||
if(tc&&tc.is_error) return 'Failed';
|
||||
if(tc&&tc.done===false) return 'Running';
|
||||
return 'Completed';
|
||||
}
|
||||
function _copyEventToClipboard(row){
|
||||
if(!row) return;
|
||||
const type=row.getAttribute('data-event-type');
|
||||
let text='';
|
||||
let label='event';
|
||||
if(type==='tool'){
|
||||
const tc=row._tcData||{};
|
||||
label=`tool ${tc.name||row.getAttribute('data-event-name')||'tool'}`;
|
||||
const parts=[`tool: ${tc.name||''}`];
|
||||
if(tc.args&&Object.keys(tc.args).length) parts.push('args: '+JSON.stringify(tc.args,null,2));
|
||||
if(tc.snippet) parts.push('output:\n'+String(tc.snippet));
|
||||
text=parts.join('\n');
|
||||
}else if(type==='thinking'){
|
||||
const pre=row.querySelector('.thinking-card-body pre');
|
||||
text=pre?pre.textContent:(row.textContent||'').replace(/^\s*Thinking\s*/i,'');
|
||||
label='thinking';
|
||||
}else{
|
||||
text=row.textContent||'';
|
||||
}
|
||||
const fallback=()=>{
|
||||
try{
|
||||
const ta=document.createElement('textarea');
|
||||
ta.value=text;
|
||||
ta.setAttribute('readonly','');
|
||||
ta.style.position='absolute';
|
||||
ta.style.left='-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
const ok=document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if(typeof showToast==='function') showToast(ok?(t('copied')||'Copied'):(t('copy_failed')||'Copy failed'),1600);
|
||||
}catch(_){
|
||||
if(typeof showToast==='function') showToast(t('copy_failed')||'Copy failed',2000,'error');
|
||||
}
|
||||
};
|
||||
if(navigator&&navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(text).then(()=>{
|
||||
if(typeof showToast==='function') showToast(`${t('copied')||'Copied'} ${label}`,1600);
|
||||
}).catch(fallback);
|
||||
}else{
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
function _attachCopyButton(header){
|
||||
if(!header) return null;
|
||||
// Reuse ANY existing copy button (handles both .transparent-event-copy
|
||||
// added by this function AND the legacy .thinking-copy-btn baked into the
|
||||
// thinking-card template). Returning the existing one prevents the
|
||||
// duplicate copy buttons that appeared in thinking boxes.
|
||||
const existing=header.querySelector('.transparent-event-copy,.thinking-copy-btn');
|
||||
if(existing){
|
||||
// Normalise the class so CSS treats them identically.
|
||||
if(!existing.classList.contains('transparent-event-copy')){
|
||||
existing.classList.add('transparent-event-copy');
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
btn=document.createElement('span');
|
||||
btn.className='transparent-event-copy';
|
||||
btn.setAttribute('role','button');
|
||||
btn.setAttribute('tabindex','0');
|
||||
btn.setAttribute('aria-label',t('copy')||'Copy');
|
||||
btn.setAttribute('data-transparent-copy','1');
|
||||
btn.title=t('copy')||'Copy';
|
||||
btn.innerHTML=`<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
||||
const handler=function(ev){
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
_copyEventToClipboard(header.closest('.transparent-event-row'));
|
||||
};
|
||||
btn.onclick=handler;
|
||||
btn.onkeydown=function(ev){
|
||||
if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();handler(ev);}
|
||||
};
|
||||
// Place the copy button at a FIXED flexbox position regardless of
|
||||
// whether a toggle or status badge is present: always right before
|
||||
// the toggle. The CSS uses flexbox order to keep it visually stable
|
||||
// even if other elements are inserted between header and toggle.
|
||||
const toggle=header.querySelector('.tool-card-toggle,.thinking-card-toggle');
|
||||
if(toggle&&toggle.parentNode===header) header.insertBefore(btn,toggle);
|
||||
else header.appendChild(btn);
|
||||
return btn;
|
||||
}
|
||||
function _transparentEventCountLabel(toolCount){
|
||||
return toolCount?`Trace: ${toolCount} ${toolCount===1?'tool':'tools'}`:'Trace';
|
||||
}
|
||||
function _setTransparentDetailMode(tab, mode){
|
||||
const row=tab&&tab.closest?tab.closest('.transparent-event-row'):null;
|
||||
const detail=row&&row.querySelector('.tool-card-detail');
|
||||
if(!detail) return;
|
||||
const next=mode==='output'?'output':'full';
|
||||
detail.setAttribute('data-transparent-detail-mode',next);
|
||||
detail.querySelectorAll('.transparent-detail-mode').forEach(el=>{
|
||||
el.classList.toggle('active', el===tab || el.getAttribute('data-mode')===next);
|
||||
});
|
||||
}
|
||||
function _setTransparentCardOpen(card, open){
|
||||
if(!card) return;
|
||||
const expanded=!!open;
|
||||
card.classList.toggle('open',expanded);
|
||||
const row=card.closest&&card.closest('.transparent-event-row');
|
||||
if(row) row.setAttribute('data-expanded',expanded?'1':'0');
|
||||
const header=card.querySelector('.tool-card-header,.thinking-card-header');
|
||||
if(header) header.setAttribute('aria-expanded',expanded?'true':'false');
|
||||
}
|
||||
function _wireTransparentHeaderToggle(header){
|
||||
if(!header||header.getAttribute('data-transparent-toggle-bound')==='1') return;
|
||||
header.setAttribute('data-transparent-toggle-bound','1');
|
||||
header.onclick=function(ev){
|
||||
const target=ev&&ev.target;
|
||||
if(target&&target.closest&&target.closest('.transparent-event-copy,.transparent-detail-mode,.tool-card-more')) return;
|
||||
const card=this.closest('.tool-card,.thinking-card');
|
||||
_setTransparentCardOpen(card,!(card&&card.classList.contains('open')));
|
||||
};
|
||||
header.onkeydown=function(ev){
|
||||
if(ev.key!=='Enter'&&ev.key!==' ') return;
|
||||
ev.preventDefault();
|
||||
const card=this.closest('.tool-card,.thinking-card');
|
||||
_setTransparentCardOpen(card,!(card&&card.classList.contains('open')));
|
||||
};
|
||||
header.setAttribute('role','button');
|
||||
header.setAttribute('tabindex','0');
|
||||
}
|
||||
function _transparentToolDetailHtml(tc, status){
|
||||
const args=tc&&tc.args&&typeof tc.args==='object'?tc.args:{};
|
||||
const argEntries=Object.entries(args);
|
||||
const meta=[
|
||||
['tool', tc&&tc.name?tc.name:'tool'],
|
||||
['status', status||_transparentToolStatus(tc)],
|
||||
];
|
||||
if(tc&&tc.duration!==undefined&&tc.duration!==null) meta.push(['duration', String(tc.duration)]);
|
||||
const preview=String((tc&&(tc.snippet||tc.preview||tc.result||tc.output))||'').trim();
|
||||
const argHtml=[...meta,...argEntries].map(([k,v])=>`<div class="tool-arg-pair"><span class="tool-arg-key">${esc(String(k))}</span><span class="tool-arg-val">${esc(typeof v==='string'?v:JSON.stringify(v,null,2))}</span></div>`).join('');
|
||||
return `<div class="tool-card-detail" data-transparent-detail-mode="full"><div class="transparent-detail-modes" role="tablist"><span class="transparent-detail-mode active" role="tab" tabindex="0" data-mode="full" onclick="_setTransparentDetailMode(this,'full')">Full</span><span class="transparent-detail-mode" role="tab" tabindex="0" data-mode="output" onclick="_setTransparentDetailMode(this,'output')">Output</span></div><div class="tool-card-args">${argHtml}</div>${preview?`<div class="tool-card-result"><pre>${esc(preview)}</pre></div>`:''}</div>`;
|
||||
}
|
||||
function _syncTransparentEventControls(turn){
|
||||
if(!turn||!isTransparentStream()) return;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return;
|
||||
const rows=Array.from(blocks.querySelectorAll(':scope > .transparent-event-row,[data-transparent-event-row="1"]'));
|
||||
const toolCount=rows.filter(row=>row.getAttribute('data-event-type')==='tool').length;
|
||||
let bar=blocks.querySelector(':scope > .transparent-event-controls');
|
||||
if(!rows.length){
|
||||
if(bar) bar.remove();
|
||||
return;
|
||||
}
|
||||
if(!bar){
|
||||
bar=document.createElement('div');
|
||||
bar.className='transparent-event-controls';
|
||||
const label=document.createElement('span');
|
||||
label.className='transparent-event-controls-label';
|
||||
label.setAttribute('data-transparent-tool-count','1');
|
||||
const expand=document.createElement('span');
|
||||
expand.className='transparent-event-control';
|
||||
expand.setAttribute('role','button');
|
||||
expand.setAttribute('tabindex','0');
|
||||
expand.setAttribute('data-transparent-expand-all','1');
|
||||
expand.textContent=t('expand_all')||'Expand all';
|
||||
expand.onclick=function(ev){ev.stopPropagation();_setTransparentRowsExpanded(this.closest('.assistant-turn'),true);};
|
||||
expand.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();ev.stopPropagation();_setTransparentRowsExpanded(this.closest('.assistant-turn'),true);}};
|
||||
const collapse=document.createElement('span');
|
||||
collapse.className='transparent-event-control';
|
||||
collapse.setAttribute('role','button');
|
||||
collapse.setAttribute('tabindex','0');
|
||||
collapse.setAttribute('data-transparent-collapse-all','1');
|
||||
collapse.textContent=t('collapse_all')||'Collapse all';
|
||||
collapse.onclick=function(ev){ev.stopPropagation();_setTransparentRowsExpanded(this.closest('.assistant-turn'),false);};
|
||||
collapse.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();ev.stopPropagation();_setTransparentRowsExpanded(this.closest('.assistant-turn'),false);}};
|
||||
bar.appendChild(label);
|
||||
bar.appendChild(expand);
|
||||
bar.appendChild(collapse);
|
||||
// Guard: firstChild may be null (empty blocks) or orphaned from a prior
|
||||
// DOM rebuild. Only insertBefore when it is still a child of blocks.
|
||||
if(blocks.firstChild&&blocks.firstChild.parentNode===blocks) blocks.insertBefore(bar, blocks.firstChild);
|
||||
else blocks.appendChild(bar);
|
||||
}
|
||||
const label=bar.querySelector('.transparent-event-controls-label');
|
||||
if(label){
|
||||
label.textContent=_transparentEventCountLabel(toolCount);
|
||||
label.setAttribute('data-transparent-tool-count',String(toolCount));
|
||||
}
|
||||
bar.setAttribute('data-tool-count',String(toolCount));
|
||||
// Wire the Hermes chat name tag toggle for the live turn.
|
||||
_wireTransparentTurnToggle(turn);
|
||||
}
|
||||
function _decorateTransparentEventRow(row, opts){
|
||||
if(!row) return row;
|
||||
opts=opts||{};
|
||||
const type=String(opts.type||row.getAttribute('data-event-type')||'event');
|
||||
row.classList.add('transparent-event-row');
|
||||
row.setAttribute('data-transparent-event-row','1');
|
||||
row.setAttribute('data-transparent-stream','1');
|
||||
row.setAttribute('data-event-type',type);
|
||||
if(opts.name) row.setAttribute('data-event-name',String(opts.name));
|
||||
if(opts.segmentSeq) row.setAttribute('data-live-segment-seq',String(opts.segmentSeq));
|
||||
if(opts.burstId) row.setAttribute('data-activity-burst-id',String(opts.burstId));
|
||||
if(type==='tool'){
|
||||
const tc=opts.toolCall||row._tcData||{};
|
||||
const name=String(opts.name||tc.name||'tool');
|
||||
row.setAttribute('data-tool-name',name);
|
||||
const status=String(opts.status||_transparentToolStatus(tc));
|
||||
row.setAttribute('data-event-status',status);
|
||||
const header=row.querySelector('.tool-card-header');
|
||||
const card=row.querySelector('.tool-card');
|
||||
if(card) card.classList.add('transparent-event-card');
|
||||
if(header){
|
||||
const nameEl=header.querySelector('.tool-card-name');
|
||||
if(nameEl) nameEl.textContent=(status==='Running'?`Running: ${_toolShortName(name)}`:_toolShortName(name));
|
||||
let statusEl=header.querySelector('.transparent-event-status');
|
||||
if(!statusEl){
|
||||
statusEl=document.createElement('span');
|
||||
statusEl.className='transparent-event-status';
|
||||
const toggle=header.querySelector('.tool-card-toggle');
|
||||
// Guard against stale toggle (see thinking-preview fix above).
|
||||
if(toggle&&toggle.parentNode===header) header.insertBefore(statusEl,toggle);
|
||||
else header.appendChild(statusEl);
|
||||
}
|
||||
if(status==='Completed'){
|
||||
statusEl.textContent='';
|
||||
statusEl.removeAttribute('data-status');
|
||||
}else{
|
||||
statusEl.textContent=status;
|
||||
statusEl.setAttribute('data-status',status.toLowerCase());
|
||||
}
|
||||
row.setAttribute('data-event-status',status);
|
||||
// Update the 3D progress bar to reflect the new status.
|
||||
const progress=card.querySelector('.transparent-event-progress');
|
||||
if(progress){
|
||||
if(status==='Completed'||status==='Failed'){
|
||||
progress.removeAttribute('data-progress-running');
|
||||
progress.setAttribute('data-progress-percent','100%');
|
||||
progress.style.setProperty('--transparent-progress-percent','100%');
|
||||
}else if(status==='Running'){
|
||||
progress.setAttribute('data-progress-running','1');
|
||||
progress.setAttribute('data-progress-percent','60%');
|
||||
progress.style.setProperty('--transparent-progress-percent','60%');
|
||||
}
|
||||
}
|
||||
let detail=row.querySelector('.tool-card-detail');
|
||||
if(!detail&&card){
|
||||
card.insertAdjacentHTML('beforeend',_transparentToolDetailHtml(tc,status));
|
||||
detail=row.querySelector('.tool-card-detail');
|
||||
if(!header.querySelector('.tool-card-toggle')){
|
||||
const toggle=document.createElement('span');
|
||||
toggle.className='tool-card-toggle';
|
||||
toggle.innerHTML=li('chevron-right',12);
|
||||
header.appendChild(toggle);
|
||||
}
|
||||
}
|
||||
if(detail&&!detail.querySelector('.transparent-detail-modes')){
|
||||
const modes=document.createElement('div');
|
||||
modes.className='transparent-detail-modes';
|
||||
modes.setAttribute('role','tablist');
|
||||
modes.innerHTML=`<span class="transparent-detail-mode active" role="tab" tabindex="0" data-mode="full" onclick="_setTransparentDetailMode(this,'full')">Full</span><span class="transparent-detail-mode" role="tab" tabindex="0" data-mode="output" onclick="_setTransparentDetailMode(this,'output')">Output</span>`;
|
||||
// Guard: firstChild may be orphaned from a prior DOM rebuild.
|
||||
const firstChild=detail.firstChild;
|
||||
if(firstChild&&firstChild.parentNode===detail) detail.insertBefore(modes, firstChild);
|
||||
else detail.appendChild(modes);
|
||||
detail.setAttribute('data-transparent-detail-mode','full');
|
||||
}
|
||||
_wireTransparentHeaderToggle(header);
|
||||
_attachCopyButton(header);
|
||||
}
|
||||
// Attach the 3D progress bar BEFORE the early return so tool rows
|
||||
// also get the bottom bar (the function used to return before
|
||||
// reaching the bar attachment, which left tool rows bar-less).
|
||||
_attachProgressBar(row, opts);
|
||||
return row;
|
||||
}
|
||||
if(type==='thinking'){
|
||||
row.classList.add('transparent-thinking-event');
|
||||
row.setAttribute('data-event-name','thinking');
|
||||
const card=row.querySelector('.thinking-card');
|
||||
const header=row.querySelector('.thinking-card-header');
|
||||
if(card) card.classList.add('transparent-event-card');
|
||||
if(header){
|
||||
const label=header.querySelector('.thinking-card-label');
|
||||
if(label) label.textContent='Thinking';
|
||||
let preview=header.querySelector('.transparent-event-preview');
|
||||
const previewText=_transparentEventPreview(opts.preview||opts.text||row.textContent||'');
|
||||
if(previewText){
|
||||
if(!preview){
|
||||
preview=document.createElement('span');
|
||||
preview.className='transparent-event-preview';
|
||||
const toggle=header.querySelector('.thinking-card-toggle');
|
||||
// Guard: toggle may be orphaned from a prior DOM rebuild
|
||||
// (renderMessages reuses row objects that have stale children).
|
||||
// Only insertBefore when toggle is still a child of header;
|
||||
// otherwise fall back to appendChild.
|
||||
if(toggle&&toggle.parentNode===header) header.insertBefore(preview,toggle);
|
||||
else header.appendChild(preview);
|
||||
}
|
||||
preview.textContent=previewText;
|
||||
}else if(preview){
|
||||
preview.remove();
|
||||
}
|
||||
_wireTransparentHeaderToggle(header);
|
||||
_attachCopyButton(header);
|
||||
}
|
||||
_attachProgressBar(row, opts);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
// ── 3D progress bar (loading path / follow-up indicator) ───────────
|
||||
// Each transparent event card has a thin 3D bar at its bottom edge.
|
||||
// While the underlying tool is running (status === 'Running') the bar
|
||||
// shows a shimmer animation; once the tool completes the bar fills to
|
||||
// 100% and stops. The bar doubles as a visual step-break between rows
|
||||
// in the stack, so the eye reads the stream as discrete steps.
|
||||
function _attachProgressBar(row, opts){
|
||||
if(!row) return;
|
||||
opts=opts||{};
|
||||
const card=row.querySelector('.tool-card,.thinking-card');
|
||||
if(!card) return;
|
||||
let bar=card.querySelector('.transparent-event-progress');
|
||||
if(!bar){
|
||||
bar=document.createElement('div');
|
||||
bar.className='transparent-event-progress';
|
||||
// Append to the card so it sits flush at the bottom edge.
|
||||
card.appendChild(bar);
|
||||
}
|
||||
// Set the running state from opts or current status.
|
||||
const status=String(opts.status||(row.getAttribute('data-event-status')||''));
|
||||
const isRunning=(status==='Running'||status==='running');
|
||||
const isCompleted=(status==='Completed'||status==='completed'||status==='Failed'||status==='failed');
|
||||
if(isRunning) bar.setAttribute('data-progress-running','1');
|
||||
else bar.removeAttribute('data-progress-running');
|
||||
if(isCompleted){
|
||||
bar.setAttribute('data-progress-percent','100%');
|
||||
bar.style.setProperty('--transparent-progress-percent','100%');
|
||||
}else if(isRunning){
|
||||
bar.setAttribute('data-progress-percent','60%');
|
||||
bar.style.setProperty('--transparent-progress-percent','60%');
|
||||
}else{
|
||||
bar.removeAttribute('data-progress-percent');
|
||||
bar.style.removeProperty('--transparent-progress-percent');
|
||||
}
|
||||
}
|
||||
function _setTransparentRowsExpanded(root, expanded){
|
||||
const scope=root||document;
|
||||
scope.querySelectorAll('.transparent-event-row .tool-card,.transparent-event-row .thinking-card').forEach(card=>{
|
||||
_setTransparentCardOpen(card,!!expanded);
|
||||
});
|
||||
}
|
||||
// ── Transparent turn-level collapse (Hermes chat name tag) ───────────────
|
||||
// In transparent_stream mode the assistant role label is the turn's "name
|
||||
// tag". Clicking it collapses the entire event stack underneath so the
|
||||
// transcript shows only the final answer (Output only). A chevron on the
|
||||
// role telegraph the affordance; the blocks body animates with the same
|
||||
// max-height transition used by individual event cards. Persisted via
|
||||
// data-attribute only — the turn's render path reads it on rebuild.
|
||||
const _transparentTurnCollapsedStates={}; // key: `${sid}:${turnMsgIdx}` → boolean
|
||||
function _wireTransparentTurnToggle(turn){
|
||||
if(!turn||turn.getAttribute('data-transparent-turn-toggle-bound')==='1') return;
|
||||
if(!isTransparentStream()) return;
|
||||
const role=turn.querySelector('.msg-role.assistant');
|
||||
if(!role) return;
|
||||
turn.setAttribute('data-transparent-turn-toggle-bound','1');
|
||||
// Add chevron if missing.
|
||||
if(!role.querySelector('.transparent-turn-chevron')){
|
||||
const chev=document.createElement('span');
|
||||
chev.className='transparent-turn-chevron';
|
||||
chev.innerHTML=li('chevron-down',10);
|
||||
role.appendChild(chev);
|
||||
}
|
||||
role.setAttribute('role','button');
|
||||
role.setAttribute('tabindex','0');
|
||||
role.setAttribute('aria-expanded',turn.getAttribute('data-transparent-turn-collapsed')==='1'?'false':'true');
|
||||
const toggle=function(ev){
|
||||
if(ev&&ev.target&&ev.target.closest&&ev.target.closest('.msg-tps-inline')) return;
|
||||
const collapsed=turn.getAttribute('data-transparent-turn-collapsed')==='1';
|
||||
turn.setAttribute('data-transparent-turn-collapsed',collapsed?'0':'1');
|
||||
role.setAttribute('aria-expanded',collapsed?'true':'false');
|
||||
// Persist state across DOM rebuilds.
|
||||
if(S.session){
|
||||
const seg=turn.querySelector('.assistant-segment');
|
||||
if(seg){
|
||||
const mi=seg.getAttribute('data-msg-idx');
|
||||
if(mi!=null) _transparentTurnCollapsedStates[`${S.session.session_id}:${mi}`]=!collapsed;
|
||||
}
|
||||
}
|
||||
};
|
||||
role.onclick=toggle;
|
||||
role.onkeydown=function(ev){
|
||||
if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();toggle(ev);}
|
||||
};
|
||||
}
|
||||
// ── Transparent old-event fading (medium → low) ───────────────────────────
|
||||
// In long streams the earliest rows fade to a lower opacity so the user's
|
||||
// eye lands on the most recent activity. The fade is per-turn: the newest
|
||||
// event stays at full opacity, each earlier event drops one step. Floors
|
||||
// at 0.32 so labels stay readable.
|
||||
function _applyTransparentRowFading(turn){
|
||||
if(!turn||!isTransparentStream()) return;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return;
|
||||
const rows=Array.from(blocks.querySelectorAll(':scope > .transparent-event-row'));
|
||||
const total=rows.length;
|
||||
for(let i=0;i<total;i++){
|
||||
const row=rows[i];
|
||||
// Newest = full opacity; each step back drops by 1 (floors at 5).
|
||||
const stepsFromEnd=total-1-i;
|
||||
if(stepsFromEnd<=0){row.removeAttribute('data-transparent-fade');continue;}
|
||||
const step=Math.min(5,stepsFromEnd);
|
||||
row.setAttribute('data-transparent-fade',String(step));
|
||||
}
|
||||
}
|
||||
// ── Transparent turn footer (elapsed · tokens · TTFT · status) ───────────
|
||||
// Mirrors the live run-status line for settled turns in transparent
|
||||
// mode. Shows duration, first-token time, token usage, and final status.
|
||||
// Only rendered for turns that have transparent event rows.
|
||||
function _transparentTurnFooterHtml(durationText, ttftText, tokensText, statusText){
|
||||
const parts=[];
|
||||
if(durationText) parts.push(`<span class="lf-time">${esc(durationText)}</span>`);
|
||||
if(ttftText) parts.push(`<span class="lf-ttft" title="${esc(t('first_token_time')||'Time to first token')}">TTFT ${esc(ttftText)}</span>`);
|
||||
if(tokensText) parts.push(`<span class="lf-tokens">${esc(tokensText)}</span>`);
|
||||
if(statusText) parts.push(`<span class="lf-status">${esc(statusText)}</span>`);
|
||||
if(!parts.length) return '';
|
||||
return `<div class="transparent-turn-footer">${parts.join('<span class="lf-sep">·</span>')}</div>`;
|
||||
}
|
||||
function _renderTransparentTurnFooter(turn, opts){
|
||||
if(!turn||!isTransparentStream()) return;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return;
|
||||
const hasRows=blocks.querySelector(':scope > .transparent-event-row');
|
||||
if(!hasRows){
|
||||
// No events → no footer (the answer itself carries the duration).
|
||||
const existing=turn.querySelector('.transparent-turn-footer');
|
||||
if(existing) existing.remove();
|
||||
return;
|
||||
}
|
||||
const durationText=opts&&opts.durationText||'';
|
||||
const ttftText=opts&&opts.ttftText||'';
|
||||
const tokensText=opts&&opts.tokensText||'';
|
||||
const statusText=opts&&opts.statusText||(t('done')||'Done');
|
||||
const html=_transparentTurnFooterHtml(durationText, ttftText, tokensText, statusText);
|
||||
let footer=turn.querySelector('.transparent-turn-footer');
|
||||
if(!html){
|
||||
if(footer) footer.remove();
|
||||
return;
|
||||
}
|
||||
if(!footer){
|
||||
footer=document.createElement('div');
|
||||
footer.className='transparent-turn-footer';
|
||||
const blocks=turn.querySelector('.assistant-turn-blocks');
|
||||
// Guard: nextSibling may be null (blocks is last child) or orphaned from
|
||||
// a prior DOM rebuild. Only insertBefore when it is still a child of turn.
|
||||
if(blocks&&blocks.nextSibling&&blocks.nextSibling.parentNode===turn){
|
||||
turn.insertBefore(footer, blocks.nextSibling);
|
||||
}else{
|
||||
turn.appendChild(footer);
|
||||
}
|
||||
}
|
||||
footer.innerHTML=html.replace(/^<div class="transparent-turn-footer">|<\/div>$/g,'');
|
||||
}
|
||||
// ── Activity-group user expand intent (#1298) ──────────────────────────────
|
||||
// When the user manually expands the live "Activity" dropdown during streaming,
|
||||
// preserve that intent across the destroy/recreate cycle that fires on every
|
||||
@@ -7117,9 +7616,6 @@ function _clearLiveRunStatusTimer(sid){
|
||||
function ensureRunActivityForCurrentTurn(){
|
||||
// Phase C: disabled — top live run Activity card removed
|
||||
return null;
|
||||
const turn=$('liveAssistantTurn');
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
return ensureRunActivityGroup(blocks,{live:true,collapsed:true});
|
||||
}
|
||||
function closeCurrentLiveActivityGroup(){
|
||||
const turn=$('liveAssistantTurn');
|
||||
@@ -8302,7 +8798,7 @@ function renderMessages(options){
|
||||
if(!isUser&&_isAssistantEmptyPlaceholderContent(m, displayContent)){
|
||||
content='';
|
||||
}
|
||||
if(!isUser&&isSimplifiedToolCalling()&&!thinkingText){
|
||||
if(!isUser&&(isCompactWorklogMode()||isTransparentStream())&&!thinkingText){
|
||||
const turnFinalVisibleContent=assistantTurnFinalVisibleContentByRawIdx.get(rawIdx)||'';
|
||||
const turnVisibleContents=assistantTurnVisibleContentByRawIdx.get(rawIdx)||[];
|
||||
thinkingText=_worklogReasoningTextFromMessage(m, rawIdx, toolCallAssistantIdxs, displayContent, turnFinalVisibleContent, turnVisibleContents);
|
||||
@@ -8381,7 +8877,7 @@ function renderMessages(options){
|
||||
seg.dataset.rawText=String(content).trim();
|
||||
if(m._activityBurstId!==undefined&&m._activityBurstId!==null) seg.setAttribute('data-activity-burst-id',String(m._activityBurstId));
|
||||
if(Number.isFinite(Number(m._liveSegmentSeq))) seg.setAttribute('data-live-segment-seq',String(Number(m._liveSegmentSeq)));
|
||||
const messageBelongsInWorklog=!S.busy&&isSimplifiedToolCalling()&&_assistantMessageBelongsInWorklog(m, rawIdx, toolCallAssistantIdxs, displayContent, {isTurnFinalAssistant});
|
||||
const messageBelongsInWorklog=!S.busy&&isCompactWorklogMode()&&_assistantMessageBelongsInWorklog(m, rawIdx, toolCallAssistantIdxs, displayContent, {isTurnFinalAssistant});
|
||||
if(messageBelongsInWorklog){
|
||||
seg.classList.add('assistant-segment-worklog-source');
|
||||
seg.setAttribute('aria-hidden','true');
|
||||
@@ -8420,7 +8916,7 @@ function renderMessages(options){
|
||||
if(_reasoningPayload) thinkingText=_reasoningPayload;
|
||||
}
|
||||
if(thinkingText&&window._showThinking!==false){
|
||||
if(isSimplifiedToolCalling()&&_assistantThinkingBelongsInWorklog(m, rawIdx, toolCallAssistantIdxs)) assistantThinking.set(rawIdx, thinkingText);
|
||||
if((isCompactWorklogMode()||isTransparentStream())&&_assistantThinkingBelongsInWorklog(m, rawIdx, toolCallAssistantIdxs)) assistantThinking.set(rawIdx, thinkingText);
|
||||
else if(window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
|
||||
}
|
||||
const hasVisibleBody=!!(String(content||'').trim()||filesHtml||statusHtml);
|
||||
@@ -8697,7 +9193,7 @@ function renderMessages(options){
|
||||
// regression vs master; same content-loss-on-switch class as #3668). The
|
||||
// `:not([data-live-thinking="1"])` / live-card guards below keep the active
|
||||
// turn's own live nodes from being double-built.
|
||||
inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking:not([data-live-thinking="1"]),.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove());
|
||||
inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking:not([data-live-thinking="1"]):not([data-event-type="thinking"]),.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove());
|
||||
const byActivity = new Map();
|
||||
const assistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b);
|
||||
const _assistantAnchorForActivity=(aIdx,segmentSeq,burstId)=>{
|
||||
@@ -8789,51 +9285,95 @@ function renderMessages(options){
|
||||
if(Number.isFinite(burstA)&&Number.isFinite(burstB)&&burstA!==burstB) return burstA-burstB;
|
||||
return a.aIdx-b.aIdx;
|
||||
});
|
||||
for(const entry of activityOrder){
|
||||
const {aIdx,segmentSeq,burstId,cards,thinkingIdx,includeAnchorReason}=entry;
|
||||
if(aIdx<assistantIdxs[0]) continue;
|
||||
const anchorRow=_assistantAnchorForActivity(aIdx,segmentSeq,burstId);
|
||||
if(!anchorRow) continue;
|
||||
const anchorParent=anchorRow.parentElement;
|
||||
const anchorReasonHtml=_worklogReasonHtmlFromAnchor(anchorRow);
|
||||
const thinkingText=thinkingIdx!==null?assistantThinking.get(thinkingIdx):'';
|
||||
if(!cards.length&&!anchorReasonHtml&&!thinkingText) continue;
|
||||
const anchorTurn=anchorRow.closest('.assistant-turn');
|
||||
if(!anchorTurn) continue;
|
||||
let state=activityByTurn.get(anchorTurn);
|
||||
if(!state){
|
||||
const includeTurnDuration=!durationAssignedTurns.has(anchorTurn);
|
||||
if(includeTurnDuration) durationAssignedTurns.add(anchorTurn);
|
||||
const activityKey=`assistant:${aIdx}`;
|
||||
const anchorIsWorklogSource=anchorRow.classList&&anchorRow.classList.contains('assistant-segment-worklog-source');
|
||||
const group=ensureActivityGroup(anchorParent,{
|
||||
collapsed:true,
|
||||
anchor:anchorRow,
|
||||
beforeAnchor:!!thinkingText&&!anchorIsWorklogSource,
|
||||
syncAnchorReason:anchorIsWorklogSource,
|
||||
activityKey,
|
||||
burstId:burstId||'',
|
||||
segmentSeq:segmentSeq||'',
|
||||
turnDuration:includeTurnDuration?_turnDurationForAnchor(anchorRow):undefined,
|
||||
if(!isTransparentStream()){
|
||||
for(const entry of activityOrder){
|
||||
const {aIdx,segmentSeq,burstId,cards,thinkingIdx,includeAnchorReason}=entry;
|
||||
if(aIdx<assistantIdxs[0]) continue;
|
||||
const anchorRow=_assistantAnchorForActivity(aIdx,segmentSeq,burstId);
|
||||
if(!anchorRow) continue;
|
||||
const anchorParent=anchorRow.parentElement;
|
||||
const anchorReasonHtml=_worklogReasonHtmlFromAnchor(anchorRow);
|
||||
const thinkingText=thinkingIdx!==null?assistantThinking.get(thinkingIdx):'';
|
||||
if(!cards.length&&!anchorReasonHtml&&!thinkingText) continue;
|
||||
const anchorTurn=anchorRow.closest('.assistant-turn');
|
||||
if(!anchorTurn) continue;
|
||||
let state=activityByTurn.get(anchorTurn);
|
||||
if(!state){
|
||||
const includeTurnDuration=!durationAssignedTurns.has(anchorTurn);
|
||||
if(includeTurnDuration) durationAssignedTurns.add(anchorTurn);
|
||||
const activityKey=`assistant:${aIdx}`;
|
||||
const anchorIsWorklogSource=anchorRow.classList&&anchorRow.classList.contains('assistant-segment-worklog-source');
|
||||
const group=ensureActivityGroup(anchorParent,{
|
||||
collapsed:true,
|
||||
anchor:anchorRow,
|
||||
beforeAnchor:!!thinkingText&&!anchorIsWorklogSource,
|
||||
syncAnchorReason:anchorIsWorklogSource,
|
||||
activityKey,
|
||||
burstId:burstId||'',
|
||||
segmentSeq:segmentSeq||'',
|
||||
turnDuration:includeTurnDuration?_turnDurationForAnchor(anchorRow):undefined,
|
||||
});
|
||||
const list=_toolWorklogListEl(group);
|
||||
if(!list) continue;
|
||||
list.innerHTML='';
|
||||
state={group,cards:[],seenReasons:new Set(),seenTools:new Set()};
|
||||
activityByTurn.set(anchorTurn,state);
|
||||
}
|
||||
state.cards.push(...cards);
|
||||
_appendWorklogStep(state.group, anchorRow, cards, thinkingText, {
|
||||
live:false,
|
||||
includeAnchorReason:!!includeAnchorReason&&!!anchorReasonHtml,
|
||||
thinkingKey:thinkingText?`thinking:${_normalizeThinkingEchoCompare(thinkingText)}`:'',
|
||||
seenReasons:state.seenReasons,
|
||||
seenTools:state.seenTools,
|
||||
});
|
||||
const list=_toolWorklogListEl(group);
|
||||
if(!list) continue;
|
||||
list.innerHTML='';
|
||||
state={group,cards:[],seenReasons:new Set(),seenTools:new Set()};
|
||||
activityByTurn.set(anchorTurn,state);
|
||||
}
|
||||
state.cards.push(...cards);
|
||||
_appendWorklogStep(state.group, anchorRow, cards, thinkingText, {
|
||||
live:false,
|
||||
includeAnchorReason:!!includeAnchorReason&&!!anchorReasonHtml,
|
||||
thinkingKey:thinkingText?`thinking:${_normalizeThinkingEchoCompare(thinkingText)}`:'',
|
||||
seenReasons:state.seenReasons,
|
||||
seenTools:state.seenTools,
|
||||
activityByTurn.forEach(state=>{
|
||||
_syncToolCallGroupSummary(state.group);
|
||||
});
|
||||
}else{
|
||||
// ── transparent_stream path: individual expandable event rows ──
|
||||
for(const entry of activityOrder){
|
||||
const event={
|
||||
...entry,
|
||||
thinkingText:entry.thinkingIdx!==null?assistantThinking.get(entry.thinkingIdx):'',
|
||||
};
|
||||
const {aIdx,segmentSeq,burstId,cards}=event;
|
||||
if(aIdx<assistantIdxs[0]) continue;
|
||||
const anchorRow=_assistantAnchorForActivity(aIdx,segmentSeq,burstId);
|
||||
if(!anchorRow) continue;
|
||||
const anchorTurn=anchorRow.closest('.assistant-turn');
|
||||
const turn=anchorTurn;
|
||||
const blocks=_assistantTurnBlocks(anchorTurn);
|
||||
if(!anchorTurn||!blocks) continue;
|
||||
const insertBefore=anchorRow&&anchorRow.parentElement===blocks?anchorRow.nextElementSibling:null;
|
||||
if(event.thinkingText){
|
||||
const thinkingRow=_decorateTransparentEventRow(_thinkingActivityNode(event.thinkingText,false),{
|
||||
type:'thinking',
|
||||
text:event.thinkingText,
|
||||
preview:event.thinkingText,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
});
|
||||
if(insertBefore&&insertBefore.parentElement===blocks) blocks.insertBefore(thinkingRow,insertBefore);
|
||||
else blocks.appendChild(thinkingRow);
|
||||
}
|
||||
for(const toolCall of cards){
|
||||
event.toolCall=toolCall;
|
||||
const toolRow=_decorateTransparentEventRow(buildToolCard(event.toolCall),{
|
||||
type:'tool',
|
||||
name:event.toolCall&&event.toolCall.name,
|
||||
status:_transparentToolStatus(event.toolCall),
|
||||
toolCall:event.toolCall,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
});
|
||||
if(insertBefore&&insertBefore.parentElement===blocks) blocks.insertBefore(toolRow,insertBefore);
|
||||
else blocks.appendChild(toolRow);
|
||||
}
|
||||
_syncTransparentEventControls(turn);
|
||||
}
|
||||
}
|
||||
activityByTurn.forEach(state=>{
|
||||
_syncToolCallGroupSummary(state.group);
|
||||
});
|
||||
}
|
||||
// Render per-turn duration and optional token usage on assistant messages.
|
||||
// Duration stays visible even when token usage is disabled, because it answers
|
||||
@@ -8853,7 +9393,7 @@ function renderMessages(options){
|
||||
// The Worklog summary owns the "Done in …" duration whenever this
|
||||
// assistant message contributes tool or thinking detail to a folded
|
||||
// Worklog above the final answer.
|
||||
const compactWorklogForMessage=isSimplifiedToolCalling()&&(toolCallAssistantIdxs.has(mi)||assistantThinking.has(mi));
|
||||
const compactWorklogForMessage=isCompactWorklogMode()&&(toolCallAssistantIdxs.has(mi)||assistantThinking.has(mi));
|
||||
const durationText=compactWorklogForMessage?'':_formatTurnDuration(msg._turnDuration);
|
||||
if(!hasTurnUsage&&!durationText&&!gatewayText&&!failoverText&&!modelWarningText) continue;
|
||||
const seg=assistantSegments.get(mi);
|
||||
@@ -8901,7 +9441,65 @@ function renderMessages(options){
|
||||
}
|
||||
if(fragments.length){
|
||||
targetFoot.classList.add('msg-foot-with-usage');
|
||||
for(let i=fragments.length-1;i>=0;i--) targetFoot.insertBefore(fragments[i], targetFoot.firstChild);
|
||||
for(let i=fragments.length-1;i>=0;i--){
|
||||
// Guard: firstChild may be null (empty foot) or orphaned.
|
||||
const firstChild=targetFoot.firstChild;
|
||||
if(firstChild&&firstChild.parentNode===targetFoot) targetFoot.insertBefore(fragments[i], firstChild);
|
||||
else targetFoot.appendChild(fragments[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transparent mode per-turn wiring: collapsible Hermes chat name tag, old-event
|
||||
// fading, and the bottom-of-turn footer (elapsed · tokens · TTFT · status).
|
||||
// Runs after the per-turn duration block above so the footer can reuse the
|
||||
// computed durationText / tokens / TTFT for each settled assistant turn.
|
||||
if(isTransparentStream()){
|
||||
for(const turn of inner.querySelectorAll('.assistant-turn')){
|
||||
if(turn.id==='liveAssistantTurn') continue;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) continue;
|
||||
const hasTransparentRows=blocks.querySelector(':scope > .transparent-event-row');
|
||||
_wireTransparentTurnToggle(turn);
|
||||
// Restore collapse state from the map (survives DOM rebuild).
|
||||
const seg=turn.querySelector('.assistant-segment');
|
||||
if(seg&&sid){
|
||||
const mi=seg.getAttribute('data-msg-idx');
|
||||
if(mi!=null&&_transparentTurnCollapsedStates[`${sid}:${mi}`]){
|
||||
turn.setAttribute('data-transparent-turn-collapsed','1');
|
||||
const role=turn.querySelector('.msg-role.assistant');
|
||||
if(role) role.setAttribute('aria-expanded','false');
|
||||
}
|
||||
}
|
||||
_applyTransparentRowFading(turn);
|
||||
if(hasTransparentRows){
|
||||
// Find the corresponding message to read duration/usage.
|
||||
const seg=turn.querySelector('.assistant-segment');
|
||||
let durationText='';
|
||||
let ttftText='';
|
||||
let tokensText='';
|
||||
if(seg){
|
||||
const mi=seg.getAttribute('data-msg-idx');
|
||||
if(mi!=null){
|
||||
const msg=S.messages[Number(mi)]||{};
|
||||
if(msg._turnDuration!=null) durationText=_formatTurnDuration(msg._turnDuration);
|
||||
if(msg._firstTokenMs!=null) ttftText=_formatFirstToken(msg._firstTokenMs);
|
||||
if(msg._turnUsage){
|
||||
const inTok=msg._turnUsage.input_tokens||0;
|
||||
const outTok=msg._turnUsage.output_tokens||0;
|
||||
tokensText=`${_fmtTokens(inTok)} in · ${_fmtTokens(outTok)} out`;
|
||||
}
|
||||
}
|
||||
}
|
||||
_renderTransparentTurnFooter(turn,{
|
||||
durationText,
|
||||
ttftText,
|
||||
tokensText,
|
||||
statusText: t('done')||'Done',
|
||||
});
|
||||
}else{
|
||||
// No transparent rows → no footer needed.
|
||||
_renderTransparentTurnFooter(turn,{});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9344,6 +9942,8 @@ function _syncToolWorklogToolGroup(group){
|
||||
steps.forEach(tools=>_syncToolRowsContainer(tools,isLiveWorklog));
|
||||
}
|
||||
function toolIcon(name){
|
||||
const raw=String(name||'');
|
||||
if(raw.startsWith('mcp__')||raw.startsWith('mcp.')) return li('plug');
|
||||
const icons={
|
||||
terminal: li('terminal'),
|
||||
read_file: li('file-text'),
|
||||
@@ -9428,6 +10028,7 @@ function buildToolCard(tc){
|
||||
const row=document.createElement('div');
|
||||
row.className='tool-card-row';
|
||||
if(!row.dataset) row.dataset={};
|
||||
row.dataset.toolName=String(tc&&tc.name||'tool');
|
||||
row.dataset.toolKind=typeof _toolActionKind==='function'?_toolActionKind(tc):'unknown';
|
||||
row.dataset.toolDone=String(tc&&tc.done!==false);
|
||||
row.dataset.toolError=String(!!(tc&&tc.is_error));
|
||||
@@ -9467,7 +10068,7 @@ function buildToolCard(tc){
|
||||
</div>
|
||||
${hasDetail?`<div class="tool-card-detail">
|
||||
${tc.args&&Object.keys(tc.args).length?`<div class="tool-card-args">${
|
||||
Object.entries(tc.args).map(([k,v])=>`<div><span class="tool-arg-key">${esc(k)}</span> <span class="tool-arg-val">${esc(String(v))}</span></div>`).join('')
|
||||
Object.entries(tc.args).map(([k,v])=>`<div class="tool-arg-pair"><span class="tool-arg-key">${esc(k)}</span><span class="tool-arg-val">${esc(String(v))}</span></div>`).join('')
|
||||
}</div>`:''}
|
||||
${displaySnippet?`<div class="tool-card-result">
|
||||
<pre>${tc.is_diff||_snippetLooksLikeDiff(displaySnippet)?`<code class="diff-block" data-highlighted="1">${_colorDiffLines(displaySnippet)}</code>`:esc(displaySnippet)}</pre>
|
||||
@@ -9664,6 +10265,55 @@ function appendLiveToolCard(tc){
|
||||
const burstAnchor=burstId?_findLatestVisibleLiveAssistantByBurst(inner, burstId):null;
|
||||
const anchor=segmentAnchor||burstAnchor||_findLatestVisibleLiveAssistant(inner)||children.filter(el=>el.matches('[data-live-assistant="1"]')).pop();
|
||||
const effectiveSegmentSeq=anchor&&anchor.getAttribute?anchor.getAttribute('data-live-segment-seq')||segmentSeq:segmentSeq;
|
||||
if(isTransparentStream()){
|
||||
const insertTransparentRow=(row)=>{
|
||||
const liveFooter=inner.querySelector('#liveRunStatus');
|
||||
const previousRows=Array.from(inner.querySelectorAll('.transparent-event-row'));
|
||||
const previous=previousRows[previousRows.length-1]||null;
|
||||
if(previous&&previous.parentElement===inner){
|
||||
previous.insertAdjacentElement('afterend',row);
|
||||
}else if(anchor&&anchor.parentElement===inner){
|
||||
anchor.insertAdjacentElement('afterend',row);
|
||||
}else if(liveFooter&&liveFooter.parentElement===inner){
|
||||
inner.insertBefore(row,liveFooter);
|
||||
}else{
|
||||
inner.appendChild(row);
|
||||
}
|
||||
};
|
||||
if(tid){
|
||||
const existing=inner.querySelector(`.transparent-event-row[data-live-tid="${CSS.escape(tid)}"],.tool-card-row[data-live-tid="${CSS.escape(tid)}"]`);
|
||||
if(existing){
|
||||
const replacement=_decorateTransparentEventRow(buildToolCard(tc),{
|
||||
type:'tool',
|
||||
name:tc&&tc.name,
|
||||
status:_transparentToolStatus(tc),
|
||||
toolCall:tc,
|
||||
segmentSeq:effectiveSegmentSeq,
|
||||
burstId,
|
||||
});
|
||||
replacement.dataset.liveTid=tid;
|
||||
existing.replaceWith(replacement);
|
||||
_syncTransparentEventControls(turn);
|
||||
_moveLiveRunStatusToTurnEnd();
|
||||
if(typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const row=_decorateTransparentEventRow(buildToolCard(tc),{
|
||||
type:'tool',
|
||||
name:tc&&tc.name,
|
||||
status:_transparentToolStatus(tc),
|
||||
toolCall:tc,
|
||||
segmentSeq:effectiveSegmentSeq,
|
||||
burstId,
|
||||
});
|
||||
if(tid) row.dataset.liveTid=tid;
|
||||
insertTransparentRow(row);
|
||||
_syncTransparentEventControls(turn);
|
||||
_moveLiveRunStatusToTurnEnd();
|
||||
if(typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return;
|
||||
}
|
||||
if(anchor) _removeEmptyLiveWorklogShells(inner);
|
||||
const group=ensureLiveWorklogContainer(inner,{
|
||||
anchor,
|
||||
@@ -9743,7 +10393,7 @@ function _findLiveAssistantAnchorForSegment(inner, segmentSeq){
|
||||
function clearLiveToolCards(){
|
||||
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||
const inner=_assistantTurnBlocks($('liveAssistantTurn'));
|
||||
if(inner) inner.querySelectorAll('.live-worklog[data-live-worklog-shell],.tool-worklog-group[data-live-tool-call-group],.tool-call-group[data-live-tool-call-group],.tool-card-row[data-live-tid]').forEach(el=>el.remove());
|
||||
if(inner) inner.querySelectorAll('.live-worklog[data-live-worklog-shell],.tool-worklog-group[data-live-tool-call-group],.tool-call-group[data-live-tool-call-group],.tool-card-row[data-live-tid]:not(.transparent-event-row)').forEach(el=>el.remove());
|
||||
// Reset the per-turn user expand intent so the next turn starts at the
|
||||
// default collapsed state (#1298).
|
||||
if(typeof _clearLiveActivityUserIntent==='function') _clearLiveActivityUserIntent();
|
||||
@@ -9774,6 +10424,11 @@ function ensureLiveWorklogShell(){
|
||||
}
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return null;
|
||||
if(isTransparentStream()){
|
||||
_moveLiveRunStatusToTurnEnd();
|
||||
scrollIfPinned();
|
||||
return blocks;
|
||||
}
|
||||
const group=ensureLiveWorklogContainer(blocks,{
|
||||
activityKey:_activityKeyForLiveTurn(),
|
||||
});
|
||||
@@ -10515,6 +11170,15 @@ function finalizeThinkingCard(){
|
||||
// stream that started it, not the session currently displayed.
|
||||
const _guardTurn = $('liveAssistantTurn');
|
||||
if(_guardTurn && S.session && _guardTurn.dataset.sessionId !== S.session.session_id) return;
|
||||
if(isTransparentStream()){
|
||||
const row=$('thinkingRow');
|
||||
if(row){
|
||||
row.removeAttribute('id');
|
||||
row.removeAttribute('data-thinking-active');
|
||||
row.removeAttribute('data-live-thinking');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!isSimplifiedToolCalling()){
|
||||
const row=$('thinkingRow');
|
||||
if(!row) return;
|
||||
@@ -10591,6 +11255,42 @@ function appendThinking(text='', options){
|
||||
burstId?`burst:${burstId}`:
|
||||
'turn'
|
||||
));
|
||||
if(isTransparentStream()){
|
||||
let row=blocks.querySelector(`.agent-activity-thinking[data-live-thinking="1"][data-live-thinking-key="${CSS.escape(thinkingKey)}"]`);
|
||||
if(!row){
|
||||
row=_thinkingActivityNode(clean, false);
|
||||
row.id='thinkingRow';
|
||||
row.setAttribute('data-live-thinking','1');
|
||||
row.setAttribute('data-live-thinking-key',thinkingKey);
|
||||
if(segmentSeq) row.setAttribute('data-live-segment-seq',segmentSeq);
|
||||
if(burstId) row.setAttribute('data-activity-burst-id',burstId);
|
||||
blocks.querySelectorAll('.agent-activity-thinking[data-thinking-active="1"]').forEach(el=>{
|
||||
if(el!==row){
|
||||
el.removeAttribute('id');
|
||||
el.removeAttribute('data-thinking-active');
|
||||
el.removeAttribute('data-live-thinking');
|
||||
}
|
||||
});
|
||||
row.setAttribute('data-thinking-active','1');
|
||||
const liveFooter=blocks.querySelector('#liveRunStatus');
|
||||
if(liveFooter&&liveFooter.parentElement===blocks) blocks.insertBefore(row,liveFooter);
|
||||
else blocks.appendChild(row);
|
||||
}else{
|
||||
_renderThinkingInto(row, clean);
|
||||
}
|
||||
row.id='thinkingRow';
|
||||
row.setAttribute('data-thinking-active','1');
|
||||
_decorateTransparentEventRow(row,{
|
||||
type:'thinking',
|
||||
text:clean,
|
||||
preview:clean,
|
||||
segmentSeq,
|
||||
burstId,
|
||||
});
|
||||
_syncTransparentEventControls(turn);
|
||||
if(typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return;
|
||||
}
|
||||
const group=ensureLiveWorklogContainer(blocks,{
|
||||
activityKey:options.activityKey||(S.activeStreamId?'live:'+S.activeStreamId:null),
|
||||
});
|
||||
@@ -10622,12 +11322,23 @@ function appendThinking(text='', options){
|
||||
}
|
||||
function updateThinking(text='', options){appendThinking(text, options);}
|
||||
function removeThinking(){
|
||||
if(isTransparentStream()){
|
||||
const liveTurn=$('liveAssistantTurn');
|
||||
const blocks=_assistantTurnBlocks(liveTurn);
|
||||
if(blocks) blocks.querySelectorAll('.agent-activity-thinking[data-thinking-active="1"]').forEach(row=>{
|
||||
row.removeAttribute('id');
|
||||
row.removeAttribute('data-thinking-active');
|
||||
row.removeAttribute('data-live-thinking');
|
||||
});
|
||||
if(liveTurn&&blocks&&!blocks.children.length) liveTurn.remove();
|
||||
return;
|
||||
}
|
||||
if(!isSimplifiedToolCalling()){
|
||||
const el=$('thinkingRow');
|
||||
if(el) el.remove();
|
||||
const turn=$('liveAssistantTurn');
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(turn&&blocks&&!blocks.children.length) turn.remove();
|
||||
const liveTurn=$('liveAssistantTurn');
|
||||
const blocks=_assistantTurnBlocks(liveTurn);
|
||||
if(liveTurn&&blocks&&!blocks.children.length) liveTurn.remove();
|
||||
return;
|
||||
}
|
||||
const turn=$('liveAssistantTurn');
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_thinking_only_turns_use_worklog_duration():
|
||||
"""Thinking-only turns now create a folded Worklog group, so that group owns
|
||||
the "Done in ..." duration instead of the final answer footer.
|
||||
"""
|
||||
m = re.search(r"const compactWorklogForMessage=isSimplifiedToolCalling\(\)&&([^;]+);", UI_JS)
|
||||
m = re.search(r"const compactWorklogForMessage=isCompactWorklogMode\(\)&&([^;]+);", UI_JS)
|
||||
assert m, "compactWorklogForMessage suppression condition not found"
|
||||
cond = m.group(1)
|
||||
assert "toolCallAssistantIdxs.has(mi)" in cond
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
"""Regression tests for issue #3820 chat activity display mode."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _transparentEventCountLabelBlock(ui_js):
|
||||
"""Return the body of `_transparentEventCountLabel` as a string slice."""
|
||||
start = ui_js.index("function _transparentEventCountLabel")
|
||||
end = ui_js.index("\nfunction ", start + 1)
|
||||
return ui_js[start:end]
|
||||
|
||||
|
||||
def test_chat_activity_display_mode_defaults_to_compact_worklog(monkeypatch, tmp_path):
|
||||
import api.config as config
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(config, "SETTINGS_FILE", settings_path)
|
||||
|
||||
loaded = config.load_settings()
|
||||
|
||||
assert loaded["chat_activity_display_mode"] == "compact_worklog"
|
||||
|
||||
|
||||
def test_chat_activity_display_mode_persists_transparent_stream_and_rejects_invalid(monkeypatch, tmp_path):
|
||||
import api.config as config
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(config, "SETTINGS_FILE", settings_path)
|
||||
|
||||
saved = config.save_settings({"chat_activity_display_mode": "transparent_stream"})
|
||||
assert saved["chat_activity_display_mode"] == "transparent_stream"
|
||||
assert json.loads(settings_path.read_text(encoding="utf-8"))["chat_activity_display_mode"] == "transparent_stream"
|
||||
|
||||
saved = config.save_settings({"chat_activity_display_mode": "invalid_mode"})
|
||||
assert saved["chat_activity_display_mode"] == "transparent_stream"
|
||||
assert json.loads(settings_path.read_text(encoding="utf-8"))["chat_activity_display_mode"] == "transparent_stream"
|
||||
|
||||
|
||||
def test_transparent_stream_uses_dedicated_mode_not_simplified_tool_calling():
|
||||
assert "function chatActivityMode()" in UI_JS
|
||||
assert "function isTransparentStream()" in UI_JS
|
||||
assert "function isCompactWorklogMode()" in UI_JS
|
||||
assert "chatActivityMode()==='transparent_stream'" in UI_JS
|
||||
assert "window._chatActivityDisplayMode" in BOOT_JS
|
||||
assert "window._chatActivityDisplayMode" in PANELS_JS
|
||||
assert "window._simplifiedToolCalling=true" in BOOT_JS
|
||||
assert "window._simplifiedToolCalling=true" in PANELS_JS
|
||||
|
||||
|
||||
def test_transparent_stream_live_branch_uses_direct_rows():
|
||||
decorator_start = UI_JS.index("function _decorateTransparentEventRow(row, opts){")
|
||||
decorator_end = UI_JS.index("function _setTransparentRowsExpanded", decorator_start)
|
||||
decorator_block = UI_JS[decorator_start:decorator_end]
|
||||
assert "data-transparent-event-row" in decorator_block
|
||||
assert "transparent-event-status" in decorator_block
|
||||
# Title carries the simple tool name (short form for MCP); status text
|
||||
# is only shown for non-default states.
|
||||
assert "_toolShortName(name)" in decorator_block
|
||||
assert "if(status==='Completed'){" in decorator_block
|
||||
assert "function _syncTransparentEventControls" in UI_JS
|
||||
assert "function _transparentEventCountLabel" in UI_JS
|
||||
assert "data-transparent-tool-count" in UI_JS
|
||||
assert "data-tool-count" in UI_JS
|
||||
# Transparent mode labels the event controls as a trace, not a Worklog
|
||||
# Activity summary.
|
||||
assert "return toolCount?`Trace: ${toolCount} ${toolCount===1?'tool':'tools'}`:'Trace';" in UI_JS
|
||||
assert "return `Activity: ${toolCount} ${toolCount===1?'tool':'tools'}`" not in UI_JS
|
||||
assert "Activity: ${parts.join(' · ')}" not in UI_JS
|
||||
# The summary must not list thinking count (DESIGN.md:75 forbids a second
|
||||
# trailing count badge, and thinking rows render as their own rows).
|
||||
assert "thinkingCount" not in _transparentEventCountLabelBlock(UI_JS)
|
||||
assert "function _setTransparentRowsExpanded" in UI_JS
|
||||
assert "function _setTransparentCardOpen" in UI_JS
|
||||
assert "function _wireTransparentHeaderToggle" in UI_JS
|
||||
assert "function _transparentToolDetailHtml" in UI_JS
|
||||
assert "function _setTransparentDetailMode" in UI_JS
|
||||
assert "transparent-detail-modes" in UI_JS
|
||||
# Spec R4: key on own line, value on next line, no inline pair.
|
||||
assert 'class="tool-arg-pair"' in UI_JS
|
||||
assert 'class="tool-arg-key"' in UI_JS
|
||||
assert 'class="tool-arg-val"' in UI_JS
|
||||
# Spec R5: controls are span[role=button], not <button> (text-link style)
|
||||
assert 'setAttribute(\'role\',\'button\')' in UI_JS
|
||||
# Spec R4: detail-mode tabs are span[role=tab], not <button>
|
||||
assert 'role="tab"' in UI_JS
|
||||
assert "transparent-detail-modes" in UI_JS
|
||||
|
||||
append_thinking_start = UI_JS.index("function appendThinking(text='', options){")
|
||||
append_thinking_end = UI_JS.index("function updateThinking", append_thinking_start)
|
||||
append_thinking_block = UI_JS[append_thinking_start:append_thinking_end]
|
||||
assert "if(isTransparentStream())" in append_thinking_block
|
||||
assert "turn=_createAssistantTurn()" in append_thinking_block
|
||||
assert "row.id='thinkingRow'" in append_thinking_block
|
||||
assert "_decorateTransparentEventRow(row,{" in append_thinking_block
|
||||
assert "type:'thinking'" in append_thinking_block
|
||||
assert "data-live-thinking-key" in append_thinking_block
|
||||
assert "_syncTransparentEventControls(turn)" in append_thinking_block
|
||||
|
||||
append_tool_start = UI_JS.index("function appendLiveToolCard(tc){")
|
||||
append_tool_end = UI_JS.index("function _findLatestLiveAssistantByBurst", append_tool_start)
|
||||
append_tool_block = UI_JS[append_tool_start:append_tool_end]
|
||||
assert "if(isTransparentStream())" in append_tool_block
|
||||
assert "_decorateTransparentEventRow(buildToolCard(tc)" in append_tool_block
|
||||
assert "_syncTransparentEventControls(turn)" in append_tool_block
|
||||
assert "ensureLiveWorklogContainer(inner" in append_tool_block # compact_worklog fallback remains intact
|
||||
|
||||
|
||||
def test_settings_ui_exposes_chat_activity_display_mode_selector():
|
||||
assert 'id="settingsChatActivityDisplayMode"' in INDEX_HTML
|
||||
assert 'data-chat-activity-mode="compact_worklog"' in INDEX_HTML
|
||||
assert 'data-chat-activity-mode="transparent_stream"' in INDEX_HTML
|
||||
assert 'value="compact_worklog"' in INDEX_HTML
|
||||
assert 'value="transparent_stream"' in INDEX_HTML
|
||||
assert 'data-i18n="settings_label_chat_activity_display_mode"' in INDEX_HTML
|
||||
assert "chat_activity_display_mode" in PANELS_JS
|
||||
assert "settingsChatActivityDisplayMode" in PANELS_JS
|
||||
assert "function _syncChatActivityDisplayModeControl" in PANELS_JS
|
||||
assert "function _pickChatActivityDisplayMode" in PANELS_JS
|
||||
assert "body.chat_activity_display_mode" in PANELS_JS
|
||||
assert "renderMessages({preserveScroll:true})" in PANELS_JS
|
||||
assert "settings_label_chat_activity_display_mode" in I18N_JS
|
||||
assert "settings_desc_chat_activity_display_mode" in I18N_JS
|
||||
|
||||
|
||||
def test_transparent_event_row_pre_3820_visual_rhythm():
|
||||
"""Restore the pre-#3820 visual rhythm: collapsed rows are flat
|
||||
list-items (no border) so the stack reads as a clean list, and tool
|
||||
rows are distinguished from thinking rows by a warm low-opacity
|
||||
secondary tint. When the user clicks a row, it expands into a small
|
||||
card with a smooth height animation.
|
||||
|
||||
Body itself stays flush with the card's border (no inner chrome), but
|
||||
contains a bordered code block for output. Status is rendered as a
|
||||
small text badge only when the tool isn't Completed.
|
||||
"""
|
||||
# Per-event card (when expanded): thin border, surface bg, radius, soft shadow.
|
||||
assert "border:1px solid var(--border-subtle)" in STYLE_CSS
|
||||
# Softened edges: 12px (was 10px, was 6px originally).
|
||||
assert "border-radius:12px" in STYLE_CSS
|
||||
assert "background:var(--surface)" in STYLE_CSS
|
||||
|
||||
# Header: single flex line, low height, hover subtle.
|
||||
# Compact rhythm: 20px header (was 22px) for tighter box size.
|
||||
assert "min-height:20px" in STYLE_CSS
|
||||
assert "display:flex" in STYLE_CSS
|
||||
|
||||
# Thinking row uses the warm low-opacity secondary tint (per-theme) PLUS
|
||||
# a thin transparent border so the box reads as a unit even when collapsed.
|
||||
assert "background:linear-gradient(90deg,color-mix(in srgb,var(--accent-bg-strong)" in STYLE_CSS
|
||||
# All transparent event boxes carry a very-low-opacity border so each box
|
||||
# is visually anchored. The thinking row's border is slightly stronger.
|
||||
assert "color-mix(in srgb,var(--accent) 58%,transparent)" in STYLE_CSS
|
||||
assert "color-mix(in srgb,var(--accent) 38%,var(--border-subtle))" in STYLE_CSS
|
||||
|
||||
# Expanded body uses visible max-height / opacity animation for smooth height.
|
||||
assert "max-height:0" in STYLE_CSS
|
||||
assert "max-height:520px" in STYLE_CSS
|
||||
assert "transition:max-height" in STYLE_CSS
|
||||
assert "data-expanded" in UI_JS
|
||||
|
||||
# Output / JSON: bordered code block, low opacity, inside the row's body.
|
||||
assert "background:var(--code-bg)" in STYLE_CSS
|
||||
assert "border:1px solid var(--border-subtle)" in STYLE_CSS
|
||||
|
||||
# Tabs: text-link style, font-weight indicates active, no pill background.
|
||||
assert ".transparent-detail-mode.active{color:var(--text);opacity:1;font-weight:600;}" in STYLE_CSS
|
||||
assert ".transparent-detail-mode.active{background:var(--hover-bg)" not in STYLE_CSS
|
||||
|
||||
# Args layout: key on own line, value on next line in monospace.
|
||||
assert ".tool-arg-pair{display:flex;flex-direction:column" in STYLE_CSS
|
||||
# Compact size: key font 10px (was 10.5px, was 11px originally) — reduced
|
||||
# by two notches for the smaller-box rhythm.
|
||||
assert ".tool-arg-key{color:var(--muted);font-size:10px" in STYLE_CSS
|
||||
assert "font-family:var(--font-mono);" in STYLE_CSS
|
||||
|
||||
# Tool rows must show a useful compact preview while collapsed.
|
||||
assert ".transparent-event-row .tool-card-preview" in STYLE_CSS
|
||||
assert ".transparent-event-row .tool-card:not(.open) .tool-card-preview" not in STYLE_CSS
|
||||
assert "display:inline" in STYLE_CSS
|
||||
|
||||
# Per-row copy button still attached for both tool and thinking rows.
|
||||
assert "function _attachCopyButton" in UI_JS
|
||||
assert "function _copyEventToClipboard" in UI_JS
|
||||
assert "navigator.clipboard.writeText" in UI_JS
|
||||
decorator_block = UI_JS[UI_JS.index("function _decorateTransparentEventRow"):UI_JS.index("function _setTransparentRowsExpanded")]
|
||||
assert decorator_block.count("_attachCopyButton(header)") >= 2
|
||||
assert "_transparentToolDetailHtml(tc,status)" in decorator_block
|
||||
assert "_wireTransparentHeaderToggle(header)" in decorator_block
|
||||
|
||||
# Bug fix: thinking blocks persist after the stream stops. removeThinking()
|
||||
# and finalizeThinkingCard() must not delete the transparent thinking row;
|
||||
# they only strip live attributes so the row can survive the settled render.
|
||||
remove_start = UI_JS.index("function removeThinking()")
|
||||
remove_end = UI_JS.index("\nfunction ", remove_start + 1)
|
||||
remove_block = UI_JS[remove_start:remove_end]
|
||||
transparent_remove_block = remove_block[remove_block.index("if(isTransparentStream())"):remove_block.index("const turn=$('liveAssistantTurn');")]
|
||||
assert "row.removeAttribute('id')" in transparent_remove_block
|
||||
assert "row.removeAttribute('data-thinking-active')" in transparent_remove_block
|
||||
assert "row.removeAttribute('data-live-thinking')" in transparent_remove_block
|
||||
assert "row.remove()" not in transparent_remove_block
|
||||
|
||||
finalize_start = UI_JS.index("function finalizeThinkingCard(){")
|
||||
finalize_end = UI_JS.index("function appendThinking", finalize_start)
|
||||
finalize_block = UI_JS[finalize_start:finalize_end]
|
||||
transparent_finalize_block = finalize_block[finalize_block.index("if(isTransparentStream())"):finalize_block.index("if(!isSimplifiedToolCalling())")]
|
||||
assert "row.removeAttribute('id')" in transparent_finalize_block
|
||||
assert "row.removeAttribute('data-thinking-active')" in transparent_finalize_block
|
||||
assert "row.removeAttribute('data-live-thinking')" in transparent_finalize_block
|
||||
assert "row.remove()" not in transparent_finalize_block
|
||||
|
||||
|
||||
def test_transparent_stream_static_branch_bypasses_worklog_summary_and_adds_event_hooks():
|
||||
marker = "// ── transparent_stream path: individual expandable event rows ──"
|
||||
start = UI_JS.index(marker)
|
||||
end = UI_JS.index("// Render per-turn duration", start)
|
||||
transparent_branch = UI_JS[start:end]
|
||||
|
||||
assert "_decorateTransparentEventRow(_thinkingActivityNode(event.thinkingText,false)" in transparent_branch
|
||||
assert "_decorateTransparentEventRow(buildToolCard(event.toolCall)" in transparent_branch
|
||||
assert "type:'thinking'" in transparent_branch
|
||||
assert "type:'tool'" in transparent_branch
|
||||
assert "_syncTransparentEventControls(turn)" in transparent_branch
|
||||
assert "_syncToolCallGroupSummary" not in transparent_branch
|
||||
assert "ensureActivityGroup" not in transparent_branch
|
||||
assert "_toolWorklogSummary" not in transparent_branch
|
||||
|
||||
render_message_start = UI_JS.index("const messageBelongsInWorklog=")
|
||||
render_message_end = UI_JS.index("if(messageBelongsInWorklog)", render_message_start)
|
||||
assert "isCompactWorklogMode()" in UI_JS[render_message_start:render_message_end]
|
||||
assert "isSimplifiedToolCalling()" not in UI_JS[render_message_start:render_message_end]
|
||||
|
||||
thinking_store_start = UI_JS.index("if(thinkingText&&window._showThinking!==false)")
|
||||
thinking_store_end = UI_JS.index("const hasVisibleBody=", thinking_store_start)
|
||||
thinking_store_block = UI_JS[thinking_store_start:thinking_store_end]
|
||||
assert "isTransparentStream()" in thinking_store_block
|
||||
assert "assistantThinking.set(rawIdx, thinkingText)" in thinking_store_block
|
||||
|
||||
|
||||
def test_fade_text_effect_uses_dynamic_window_check():
|
||||
"""The fade text effect must read window._fadeTextEffect dynamically
|
||||
on every call, and the Settings checkbox must update the live window flag
|
||||
immediately so the current session can start fading without a reload."""
|
||||
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
# Locate the helper and confirm it reads the live value.
|
||||
helper_start = MESSAGES_JS.index("function _shouldUseStreamFade(")
|
||||
helper_end = MESSAGES_JS.index("\n function ", helper_start + 1)
|
||||
helper_block = MESSAGES_JS[helper_start:helper_end]
|
||||
assert "window._fadeTextEffect" in helper_block
|
||||
# Should not return the captured const any more.
|
||||
assert "return _streamFadeEnabledForStream;" not in helper_block
|
||||
|
||||
# The preferences listener must update the runtime flag immediately, not
|
||||
# only after autosave/save completes.
|
||||
fade_cb_start = PANELS_JS.index("const fadeTextCb=$('settingsFadeTextEffect');", 300000)
|
||||
fade_cb_end = PANELS_JS.index("const terminalAutoExpandCb", fade_cb_start)
|
||||
fade_cb_block = PANELS_JS[fade_cb_start:fade_cb_end]
|
||||
assert "window._fadeTextEffect=fadeTextCb.checked" in fade_cb_block
|
||||
assert "fadeTextCb.addEventListener('change',()=>{" in fade_cb_block
|
||||
assert "_schedulePreferencesAutosave();" in fade_cb_block
|
||||
# The change handler must not be the old direct autosave callback.
|
||||
assert "addEventListener('change',_schedulePreferencesAutosave" not in fade_cb_block
|
||||
|
||||
|
||||
def test_thinking_blocks_persist_after_renderMessages():
|
||||
"""renderMessages() rebuilds the settled DOM and used to wipe every
|
||||
.agent-activity-thinking row that lacked data-live-thinking="1" —
|
||||
including the promoted permanent event rows that removeThinking()
|
||||
now leaves behind. Guard the selector so promoted thinking rows
|
||||
survive the rebuild."""
|
||||
inner_sweep = ".agent-activity-thinking:not([data-live-thinking=\"1\"]):not([data-event-type=\"thinking\"])"
|
||||
assert inner_sweep in UI_JS
|
||||
# The promoted-row guard is the only thing that changed; the rest of
|
||||
# the selector must remain intact.
|
||||
head = ".tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking"
|
||||
tail = ".wl-reason[data-worklog-reason-source=\"reasoning\"]"
|
||||
assert head in UI_JS
|
||||
assert tail in UI_JS
|
||||
|
||||
|
||||
def test_mcp_tool_names_appear_in_event_rows():
|
||||
"""MCP (Model Context Protocol) tools are namespaced as
|
||||
mcp__<server>__<tool>. Their full name must be preserved on the row's
|
||||
data-tool-name attribute, the header must use a friendly short form
|
||||
(e.g. github/create_issue), and a plug-style icon must be used when
|
||||
no canonical Hermes icon matches."""
|
||||
# toolIcon: plug icon for mcp__ / mcp. prefixed names.
|
||||
assert "startsWith('mcp__')" in UI_JS
|
||||
assert "startsWith('mcp.')" in UI_JS
|
||||
assert "li('plug')" in UI_JS
|
||||
# _toolShortName reduces the namespace; the decorator must use it.
|
||||
assert "function _toolShortName" in UI_JS
|
||||
decorator_block = UI_JS[UI_JS.index("function _decorateTransparentEventRow"):UI_JS.index("function _setTransparentRowsExpanded")]
|
||||
assert "_toolShortName(name)" in decorator_block
|
||||
# buildToolCard preserves the full tool name on data-tool-name.
|
||||
assert 'dataset.toolName' in UI_JS or 'row.dataset.toolName' in UI_JS
|
||||
|
||||
|
||||
def test_transparent_turn_header_is_collapsible():
|
||||
"""The Hermes chat name tag (assistant role label) must be clickable
|
||||
in transparent mode to collapse/expand the entire event stack
|
||||
underneath. A chevron is appended to the role to telegraph the
|
||||
affordance; toggling flips data-transparent-turn-collapsed on the
|
||||
turn and the CSS collapses the blocks body via max-height."""
|
||||
assert "function _wireTransparentTurnToggle" in UI_JS
|
||||
assert "data-transparent-turn-collapsed" in UI_JS
|
||||
assert "transparent-turn-chevron" in UI_JS
|
||||
# The CSS must collapse the blocks when the data attribute is set.
|
||||
assert 'data-transparent-turn-collapsed="1"] .transparent-event-row' in STYLE_CSS
|
||||
# The chevron rotates on collapse.
|
||||
assert "transparent-turn-chevron" in STYLE_CSS
|
||||
assert "transform:rotate(-90deg)" in STYLE_CSS
|
||||
|
||||
|
||||
def test_old_event_fading_medium_to_low():
|
||||
"""Older transparent event rows must fade to lower opacity (medium
|
||||
→ low) so the user's eye lands on the most recent activity. The
|
||||
fade is per-turn and per-row position with a floor at 0.32."""
|
||||
assert "function _applyTransparentRowFading" in UI_JS
|
||||
# CSS steps from medium (~0.82) to low (~0.32).
|
||||
assert 'data-transparent-fade="1"' in STYLE_CSS
|
||||
assert 'data-transparent-fade="5"' in STYLE_CSS
|
||||
assert "opacity:.32" in STYLE_CSS
|
||||
# Hover restores full opacity.
|
||||
assert ".transparent-event-row[data-transparent-fade]:hover{opacity:1" in STYLE_CSS
|
||||
|
||||
|
||||
def test_transparent_turn_footer_shows_elapsed_tokens_ttft_status():
|
||||
"""The bottom-of-turn footer mirrors the live run-status line for
|
||||
settled turns: duration, first-token time, token usage, and final
|
||||
status. Only renders for turns that have transparent event rows."""
|
||||
assert "function _renderTransparentTurnFooter" in UI_JS
|
||||
assert "function _formatFirstToken" in UI_JS
|
||||
assert "transparent-turn-footer" in STYLE_CSS
|
||||
assert "lf-ttft" in STYLE_CSS
|
||||
assert "lf-time" in STYLE_CSS
|
||||
assert "lf-status" in STYLE_CSS
|
||||
# The footer must read duration, TTFT, and tokens from the message.
|
||||
assert "_firstTokenMs" in UI_JS
|
||||
assert "_formatFirstToken(msg._firstTokenMs)" in UI_JS
|
||||
assert "_formatTurnDuration(msg._turnDuration)" in UI_JS
|
||||
# i18n key for the TTFT tooltip.
|
||||
assert "first_token_time" in I18N_JS
|
||||
|
||||
|
||||
def test_transparent_turn_wiring_runs_after_per_turn_duration_block():
|
||||
"""The turn-level wiring (toggle + fading + footer) must run AFTER
|
||||
the per-turn duration block so the footer can reuse the computed
|
||||
duration / tokens / TTFT for each settled turn."""
|
||||
wiring_idx = UI_JS.index("// Transparent mode per-turn wiring:")
|
||||
duration_block_end = UI_JS.index("}", UI_JS.index("targetFoot.classList.add('msg-foot-with-usage')"))
|
||||
assert duration_block_end != -1
|
||||
assert wiring_idx > duration_block_end, (
|
||||
"transparent turn wiring must run after the per-turn duration block "
|
||||
"so the footer can reuse computed duration/tokens/TTFT"
|
||||
)
|
||||
|
||||
|
||||
def test_three_d_progress_bar_attached_to_each_event_row():
|
||||
"""Each transparent event card has a 3D progress bar at its bottom
|
||||
edge. The bar shimmers while the tool is running and fills to 100%
|
||||
when it completes. Drawn as a 1px highlight + 1px shadow for the
|
||||
3D ridge effect, and uses a step-break pseudo-element between
|
||||
consecutive rows so the stream reads as discrete steps."""
|
||||
assert "function _attachProgressBar" in UI_JS
|
||||
# The decorator wires the progress bar at the end of the function.
|
||||
assert "_attachProgressBar(row, opts)" in UI_JS
|
||||
# CSS: bar has a 3D ridge (::after highlight) and a shimmer keyframe.
|
||||
assert ".transparent-event-progress" in STYLE_CSS
|
||||
assert "transparent-progress-shimmer" in STYLE_CSS
|
||||
assert "@keyframes transparent-progress-shimmer" in STYLE_CSS
|
||||
# The bar lives at the bottom of the card with a 12px radius to match
|
||||
# the card's bottom-left and bottom-right corners.
|
||||
assert "border-radius:0 0 12px 12px" in STYLE_CSS
|
||||
# The 3D ridge highlight.
|
||||
assert "transparent-event-progress::after" in STYLE_CSS
|
||||
# Step break: between two consecutive event rows, a soft gradient line.
|
||||
assert ".transparent-event-row + .transparent-event-row::before" in STYLE_CSS
|
||||
|
||||
|
||||
def test_copy_button_position_is_stable_and_dedup_handles_legacy_template():
|
||||
"""The copy button uses flex `order:9` so its position is stable
|
||||
regardless of which other elements (status, preview) are in the
|
||||
header. The toggle uses `order:10` so copy + toggle cluster at the
|
||||
right edge in a predictable order. _attachCopyButton also reuses
|
||||
the legacy .thinking-copy-btn baked into the thinking-card HTML
|
||||
template, preventing duplicate copy buttons in the thinking box."""
|
||||
# Flex order pinning.
|
||||
assert "order:9" in STYLE_CSS
|
||||
assert "order:10" in STYLE_CSS
|
||||
# Dedup: _attachCopyButton checks for both .transparent-event-copy
|
||||
# and .thinking-copy-btn.
|
||||
assert "'.transparent-event-copy,.thinking-copy-btn'" in UI_JS
|
||||
# The function normalises the class so CSS treats them identically.
|
||||
assert "existing.classList.add('transparent-event-copy')" in UI_JS
|
||||
|
||||
|
||||
def test_increased_transparency_and_softened_edges():
|
||||
"""Background opacity is slightly lower (22% accent for tool, 30% for
|
||||
thinking vs 28%/38% before) and the border is softer (12% accent
|
||||
mix). Edges are softer: 12px border-radius, lower box-shadow
|
||||
opacity, and the hover/expanded shadows are gentler."""
|
||||
# Softer background.
|
||||
assert "color-mix(in srgb,var(--accent-bg) 22%,transparent)" in STYLE_CSS
|
||||
# Softer border.
|
||||
assert "color-mix(in srgb,var(--accent) 12%,var(--border-subtle))" in STYLE_CSS
|
||||
# Softer shadow.
|
||||
assert "rgba(0,0,0,.03)" in STYLE_CSS
|
||||
# 12px edges.
|
||||
assert "border-radius:12px" in STYLE_CSS
|
||||
|
||||
|
||||
def test_smaller_tool_icons_and_reduced_font_sizes():
|
||||
"""Tool icons are constrained to 11px (was 14px), and the tool name
|
||||
font is 11.5px (was 12.5px — reduced by two notches). The arg-key
|
||||
font is 10px (was 11px). Status badge gets a pill background and
|
||||
letter-spacing for visual distinction from the copy button."""
|
||||
# Icon wrapper size.
|
||||
assert "width:14px;height:14px" in STYLE_CSS
|
||||
# Inner SVG size.
|
||||
assert "width:11px;height:11px" in STYLE_CSS
|
||||
# Tool name font 11.5px (down from 12.5px).
|
||||
assert "font-size:11.5px" in STYLE_CSS
|
||||
# Status badge: pill background + letter-spacing.
|
||||
assert "border-radius:6px" in STYLE_CSS
|
||||
assert "letter-spacing:.02em" in STYLE_CSS
|
||||
@@ -63,6 +63,7 @@ def test_last_assistant_keeps_usage_visible_and_reveals_time_and_actions_on_hove
|
||||
assert (
|
||||
"targetFoot.insertBefore(usage, targetFoot.firstChild);" in UI_JS
|
||||
or "targetFoot.insertBefore(fragments[i], targetFoot.firstChild);" in UI_JS
|
||||
or "targetFoot.insertBefore(fragments[i], firstChild);" in UI_JS
|
||||
)
|
||||
assert ".assistant-turn .msg-foot-with-usage," in UI_CSS
|
||||
assert ".msg-row[data-role=\"assistant\"] .msg-foot-with-usage {\n opacity: 1;" in UI_CSS
|
||||
|
||||
Reference in New Issue
Block a user