feat(#484): configurable default view for JSON/YAML code blocks

The JSON/YAML tree viewer (#484) hardcoded its default: valid json/yaml
fenced blocks opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`).
Make that default user-configurable while preserving current behavior.

- New settings (persisted via /api/settings -> settings.json):
  - structured_code_default_view: auto | on | off  (default auto)
  - structured_code_auto_tree_lines: int 1..1000    (default 10)
- ui.js: replace the hardcode with a sanitized, unit-testable decision
  helper _structuredCodeShowTree(mode, threshold, lineCount); Tree/Raw
  toggle, Raw <pre><code>, parse-failure fallback, and YAML lazy-load
  are unchanged.
- Appearance settings: 'JSON/YAML code blocks' select + 'Auto threshold
  lines' input (disabled outside auto mode); wired through the existing
  appearance-autosave and explicit-save paths; live re-render on change.
- boot.js seeds window globals; i18n labels added to all 13 locales.
- Default (auto + 10) reproduces the original >=10 behavior exactly.
This commit is contained in:
Charles Bine committed 2026-06-23 13:51:45 -04:00
1 parent 46f5fb93fe
commit dd66640fae
7 files changed
+406 -6

No files matched your search

+4
View File
@@ -7343,6 +7343,8 @@ _SETTINGS_DEFAULTS = {
"font_size": "default", # small | default | large | xlarge
"session_jump_buttons": False, # show Start/End transcript jump pills
"render_user_markdown": False, # opt-in: render full markdown in user messages (#3870)
"structured_code_default_view": "auto", # JSON/YAML fenced-block default render: auto | on | off (#484 follow-up). auto => Tree when line count >= structured_code_auto_tree_lines, else Raw.
"structured_code_auto_tree_lines": 10, # in 'auto' mode, minimum line count to default a JSON/YAML block to Tree view (preserves the original hardcoded >=10 behavior)
"session_endless_scroll": False, # auto-load older transcript pages while scrolling upward
"chat_activity_display_mode": "compact_worklog", # compact_worklog | transparent_stream
"auto_scroll_follow": True, # follow new output to the bottom while streaming (Codex/Claude-Code-style sticky bottom); the user scrolling up unpins and is respected
@@ -7551,6 +7553,7 @@ _SETTINGS_ENUM_VALUES = {
"auto_title_refresh_every": {"0", "5", "10", "20"},
"busy_input_mode": {"queue", "interrupt", "steer"},
"chat_activity_display_mode": {"compact_worklog", "transparent_stream"},
"structured_code_default_view": {"auto", "on", "off"},
}
_SETTINGS_INT_RANGES = {
"pinned_sessions_limit": (1, 99),
@@ -7559,6 +7562,7 @@ _SETTINGS_INT_RANGES = {
"inflight_state_max_tool_calls": (1, 200),
"inflight_state_max_string_chars": (1000, 500000),
"inflight_state_max_json_chars": (100000, 4000000),
"structured_code_auto_tree_lines": (1, 1000),
}
_SETTINGS_BOOL_KEYS = {
"onboarding_completed",
+7
View File
@@ -2158,6 +2158,11 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
}
window._sessionJumpButtonsEnabled=!!s.session_jump_buttons;
window._renderUserMarkdown=!!s.render_user_markdown;
// JSON/YAML structured code-block default view (#484): auto | on | off,
// plus the 'auto'-mode line threshold (sanitized int 1..1000, fallback 10).
window._structuredCodeDefaultView=['on','off','auto'].includes(s.structured_code_default_view)?s.structured_code_default_view:'auto';
const _sctLines=parseInt(s.structured_code_auto_tree_lines,10);
window._structuredCodeAutoTreeLines=(Number.isFinite(_sctLines)&&_sctLines>=1&&_sctLines<=1000)?_sctLines:10;
// Reconcile appearance: prefer localStorage (what the user last saw) over
// the server. If they diverge (e.g. a previous autosave POST failed),
// push the localStorage values back to the server so settings.json stays
@@ -2227,6 +2232,8 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
window._workspaceTodosTab=false;
if(typeof _applyWorkspaceTodosTabVisibility==='function') _applyWorkspaceTodosTabVisibility();
window._sessionJumpButtonsEnabled=false;
window._structuredCodeDefaultView='auto';
window._structuredCodeAutoTreeLines=10;
window._sidebarDensity='compact';
window._pinnedSessionsLimit=3;
window._busyInputMode='queue';
+78
View File
@@ -598,6 +598,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar',
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.',
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -2171,6 +2177,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -3735,6 +3747,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
settings_label_worklog_details_expanded_default: 'Worklog の詳細を自動的に開く',
settings_desc_worklog_details_expanded_default: '有効にすると、新しい Worklog の詳細が展開された状態で始まり、ツール、Thinking、進捗カードをクリックなしで確認できます。無効の場合、Worklog の詳細はデフォルトで折りたたまれます。ターンごとの手動折りたたみ/展開は優先されます。',
settings_label_chat_activity_display_mode: 'アクティビティ表示',
@@ -6000,6 +6018,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -7489,6 +7513,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -8659,6 +8689,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -10502,6 +10538,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -11304,6 +11346,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: '在標題列顯示設定檔切換器',
settings_desc_show_titlebar_profile: '啟用後,左上角的應用程式標題列會出現設定檔切換按鈕,讓你在任何分頁都能切換設定檔。預設關閉;無論此設定為何,輸入列底部一律保留設定檔切換器。',
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -12764,6 +12812,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -14230,6 +14284,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -15718,6 +15778,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -17300,6 +17366,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
@@ -18878,6 +18950,12 @@ const LOCALES = {
settings_label_show_titlebar_profile: 'Show profile switcher in titlebar', // TODO: translate
settings_desc_show_titlebar_profile: 'When enabled, a profile switcher button appears in the top-left app titlebar so you can change profiles from any tab. Off by default; the composer footer always has a profile switcher regardless of this setting.', // TODO: translate
settings_desc_render_user_markdown: 'When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.',
settings_label_structured_code: 'JSON/YAML code blocks',
settings_option_structured_code_auto: 'Auto: Tree for long blocks',
settings_option_structured_code_on: 'Tree by default',
settings_option_structured_code_off: 'Raw by default',
settings_label_structured_code_auto_lines: 'Auto threshold lines',
settings_desc_structured_code: 'When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.',
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',
+13
View File
@@ -1071,6 +1071,19 @@
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_render_user_markdown">When enabled, bold, italic, links, and other markdown in your own messages are rendered. Off by default; fenced code blocks and math always render regardless of this setting.</div>
</div>
<div class="settings-field" style="margin-top:8px">
<label for="settingsStructuredCodeMode" data-i18n="settings_label_structured_code">JSON/YAML code blocks</label>
<select id="settingsStructuredCodeMode" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">
<option value="auto" data-i18n="settings_option_structured_code_auto">Auto: Tree for long blocks</option>
<option value="on" data-i18n="settings_option_structured_code_on">Tree by default</option>
<option value="off" data-i18n="settings_option_structured_code_off">Raw by default</option>
</select>
<div style="margin-top:8px">
<label for="settingsStructuredCodeAutoLines" data-i18n="settings_label_structured_code_auto_lines">Auto threshold lines</label>
<input type="number" id="settingsStructuredCodeAutoLines" min="1" max="1000" step="1" inputmode="numeric" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
</div>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_structured_code">When Auto is selected, JSON/YAML blocks with at least this many lines open in Tree view. Tree always opens the structured viewer; Raw keeps the syntax-highlighted text. You can still switch Tree/Raw on each block.</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="settingsShowTitlebarProfile" style="width:15px;height:15px;accent-color:var(--accent)">
+81
View File
@@ -7002,6 +7002,44 @@ function _applyTtsEnabled(enabled){
document.body.classList.toggle('tts-enabled', !!enabled);
}
// Read + sanitize the JSON/YAML structured code-block default-view controls
// (#484). mode is one of auto|on|off; lines is clamped to an int 1..1000 with a
// fallback of 10 (the original hardcoded threshold).
function _structuredCodeViewFromUi(){
const modeSel=$('settingsStructuredCodeMode');
const mode=modeSel&&['auto','on','off'].includes(modeSel.value)?modeSel.value:'auto';
const linesField=$('settingsStructuredCodeAutoLines');
const n=parseInt((linesField||{}).value,10);
const lines=(Number.isFinite(n)&&n>=1&&n<=1000)?n:10;
return {structured_code_default_view:mode,structured_code_auto_tree_lines:lines};
}
// Apply the structured code-block settings to runtime globals and re-render the
// transcript so already-rendered JSON/YAML blocks pick up the new default. The
// per-block Raw/Tree toggle is unaffected.
function _applyStructuredCodeViewSettings(mode,lines,rerender){
window._structuredCodeDefaultView=['auto','on','off'].includes(mode)?mode:'auto';
const n=parseInt(lines,10);
window._structuredCodeAutoTreeLines=(Number.isFinite(n)&&n>=1&&n<=1000)?n:10;
if(rerender){
if(typeof clearMessageRenderCache==='function') clearMessageRenderCache();
if(typeof renderMessages==='function') renderMessages({preserveScroll:true});
}
}
// The Auto-threshold input is only meaningful in 'auto' mode; disable it
// otherwise so the control reads as inactive without hiding it.
function _syncStructuredCodeLinesEnabled(){
const modeSel=$('settingsStructuredCodeMode');
const linesField=$('settingsStructuredCodeAutoLines');
// Both controls live in the same settings-field and are present together;
// if either is missing there's nothing to sync.
if(!modeSel||!linesField) return;
const isAuto=modeSel.value==='auto';
linesField.disabled=!isAuto;
linesField.style.opacity=isAuto?'':'0.5';
}
function _appearancePayloadFromUi(){
const worklogDetailsExpanded=!!($('settingsWorklogDetailsExpandedDefault')||{}).checked;
const chatActivityModeSel=$('settingsChatActivityDisplayMode');
@@ -7014,6 +7052,7 @@ function _appearancePayloadFromUi(){
session_endless_scroll: !!($('settingsSessionEndlessScroll')||{}).checked,
auto_scroll_follow: !!($('settingsAutoScrollFollow')||{}).checked,
render_user_markdown: !!($('settingsRenderUserMarkdown')||{}).checked,
..._structuredCodeViewFromUi(),
show_titlebar_profile: !!($('settingsShowTitlebarProfile')||{}).checked,
worklog_details_expanded_default: worklogDetailsExpanded,
activity_feed_expanded_default: worklogDetailsExpanded,
@@ -7102,6 +7141,16 @@ async function _autosaveAppearanceSettings(payload){
}
window._sessionEndlessScrollEnabled=!!(saved&&saved.session_endless_scroll);
window._autoScrollFollow=!saved||saved.auto_scroll_follow!==false;
if(saved&&Object.prototype.hasOwnProperty.call(saved,'structured_code_default_view')){
// Re-sync from the server-validated/clamped values so the UI and runtime
// globals match exactly what was persisted.
_applyStructuredCodeViewSettings(saved.structured_code_default_view,saved.structured_code_auto_tree_lines,false);
const modeSel=$('settingsStructuredCodeMode');
if(modeSel) modeSel.value=window._structuredCodeDefaultView;
const linesField=$('settingsStructuredCodeAutoLines');
if(linesField) linesField.value=window._structuredCodeAutoTreeLines;
_syncStructuredCodeLinesEnabled();
}
if(saved&&payload&&Object.prototype.hasOwnProperty.call(payload,'worklog_details_expanded_default')&&(
Object.prototype.hasOwnProperty.call(saved,'worklog_details_expanded_default') ||
Object.prototype.hasOwnProperty.call(saved,'activity_feed_expanded_default')
@@ -7406,6 +7455,34 @@ async function loadSettingsPanel(){
_scheduleAppearanceAutosave();
};
}
const structuredCodeModeSel=$('settingsStructuredCodeMode');
const structuredCodeLinesField=$('settingsStructuredCodeAutoLines');
if(structuredCodeModeSel){
const mode=['auto','on','off'].includes(settings.structured_code_default_view)?settings.structured_code_default_view:'auto';
structuredCodeModeSel.value=mode;
const lines=parseInt(settings.structured_code_auto_tree_lines,10);
const safeLines=(Number.isFinite(lines)&&lines>=1&&lines<=1000)?lines:10;
if(structuredCodeLinesField) structuredCodeLinesField.value=safeLines;
_applyStructuredCodeViewSettings(mode,safeLines,false);
_syncStructuredCodeLinesEnabled();
structuredCodeModeSel.onchange=function(){
const cfg=_structuredCodeViewFromUi();
_applyStructuredCodeViewSettings(cfg.structured_code_default_view,cfg.structured_code_auto_tree_lines,true);
_syncStructuredCodeLinesEnabled();
_scheduleAppearanceAutosave();
};
if(structuredCodeLinesField){
// Commit on change (blur / Enter / spinner) rather than every keystroke,
// so a long transcript isn't rebuilt per digit typed. Only re-render in
// 'auto' mode, where the threshold actually affects the default view.
structuredCodeLinesField.addEventListener('change',function(){
const cfg=_structuredCodeViewFromUi();
structuredCodeLinesField.value=cfg.structured_code_auto_tree_lines;
_applyStructuredCodeViewSettings(cfg.structured_code_default_view,cfg.structured_code_auto_tree_lines,cfg.structured_code_default_view==='auto');
_scheduleAppearanceAutosave();
},{once:false});
}
}
const showTitlebarProfileCb=$('settingsShowTitlebarProfile');
if(showTitlebarProfileCb){
showTitlebarProfileCb.checked=!!settings.show_titlebar_profile;
@@ -9091,6 +9168,9 @@ function _applySavedSettingsUi(saved, body, opts){
window._busyInputMode=body.busy_input_mode||'queue';
window._sessionEndlessScrollEnabled=!!body.session_endless_scroll;
window._autoScrollFollow=body.auto_scroll_follow!==false;
if(Object.prototype.hasOwnProperty.call(body,'structured_code_default_view')){
_applyStructuredCodeViewSettings(body.structured_code_default_view,body.structured_code_auto_tree_lines,false);
}
window._botName=body.bot_name||'Hermes';
if(typeof applyBotName==='function') applyBotName();
if(typeof setLocale==='function') setLocale(language);
@@ -9612,6 +9692,7 @@ async function saveSettings(andClose){
body.chat_activity_display_mode=(($('settingsChatActivityDisplayMode')||{}).value==='transparent_stream')?'transparent_stream':'compact_worklog';
body.auto_scroll_follow=!!($('settingsAutoScrollFollow')||{}).checked;
body.render_user_markdown=!!($('settingsRenderUserMarkdown')||{}).checked;
Object.assign(body,_structuredCodeViewFromUi());
body.language=language;
body.show_token_usage=showTokenUsage;
body.show_quota_chip=showQuotaChip===true;
+33 -2
View File
@@ -13437,6 +13437,34 @@ function _loadJsyamlThen(cb){
document.head.appendChild(s);
}
// ── JSON/YAML structured code-block default-view configuration (#484) ──
// Read the user's configured default-view mode for valid JSON/YAML fenced
// blocks. Falls back to 'auto' for any missing/invalid value so the renderer
// stays safe before settings load and in non-browser test contexts.
function _structuredCodeMode(){
const m=(typeof window!=='undefined')?window._structuredCodeDefaultView:undefined;
return (m==='on'||m==='off'||m==='auto')?m:'auto';
}
// Read the configured 'auto'-mode line threshold, clamped to a sane integer
// range. Invalid/missing values fall back to 10 (the original hardcoded value).
function _structuredCodeThreshold(){
const raw=(typeof window!=='undefined')?window._structuredCodeAutoTreeLines:undefined;
const n=parseInt(raw,10);
return (Number.isFinite(n)&&n>=1&&n<=1000)?n:10;
}
// Pure decision helper: should a structured block default to Tree view?
// Factored out so the (mode, threshold, lineCount) contract is unit-testable.
// mode 'on' => always Tree
// mode 'off' => always Raw
// mode 'auto' => Tree only when lineCount >= threshold (threshold sanitized,
// fallback 10)
function _structuredCodeShowTree(mode,threshold,lineCount){
if(mode==='on') return true;
if(mode==='off') return false;
const th=(Number.isFinite(threshold)&&threshold>=1&&threshold<=1000)?threshold:10;
return lineCount>=th;
}
function initTreeViews(container){
const root=container||document;
root.querySelectorAll('.code-tree-wrap:not([data-tree-init])').forEach(wrap=>{
@@ -13474,8 +13502,11 @@ function initTreeViews(container){
return; // leave as raw view
}
const lineCount=rawText.split('\n').length;
// Default to raw for short blocks (<10 lines), tree for longer
const showTree=lineCount>=10;
// Default view is user-configurable (#484 follow-up). 'on' => always Tree,
// 'off' => always Raw, 'auto' => Tree only when the block is >= the
// configured line threshold (default 10, preserving the original behavior).
// The per-block Raw/Tree toggle below always remains available regardless.
const showTree=_structuredCodeShowTree(_structuredCodeMode(),_structuredCodeThreshold(),lineCount);
// Build tree DOM
const treeDiv=document.createElement('div');
treeDiv.className='tree-view'+(showTree?'':' tree-hidden');
+190 -4
View File
@@ -1,7 +1,32 @@
"""Tests for issue #484 — collapsible JSON/YAML tree viewer."""
"""Tests for issue #484 — collapsible JSON/YAML tree viewer.
The original feature hardcoded the default view: valid JSON/YAML fenced blocks
opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`). That default
is now user-configurable (#484 follow-up) via two settings:
- structured_code_default_view: "auto" | "on" | "off"
on => always default to Tree
off => always default to Raw
auto => Tree only when the block line count >= the configured threshold
- structured_code_auto_tree_lines: integer 1..1000 (default 10)
`auto` + threshold 10 reproduces the original behavior, so the default is
preserved. These tests pin the new configurable shape while keeping the existing
Tree/Raw renderer invariants (wrapper class, helpers, value types, toggle, YAML
support) intact.
"""
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent.resolve()
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")
class TestTreeRenderer:
"""Fenced JSON/YAML blocks should get a tree view toggle."""
@@ -68,11 +93,125 @@ class TestTreeRenderer:
content = f.read()
assert "jsyaml" in content
def test_short_json_defaults_to_raw(self):
"""Blocks under 10 lines should default to raw view."""
class TestConfigurableDefaultView:
"""The default Tree/Raw decision is configurable, not hardcoded (#484 follow-up)."""
def test_hardcoded_threshold_is_gone(self):
"""The original `lineCount>=10` hardcode must no longer drive the default."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "lineCount>=10" in content
assert "const showTree=lineCount>=10;" not in content, (
"The hardcoded `const showTree=lineCount>=10;` must be replaced by the "
"configurable decision helper."
)
assert "lineCount>=10" not in content, (
"No hardcoded `lineCount>=10` comparison should remain; the threshold "
"is configurable and the default is read from a sanitized helper."
)
def test_decision_helper_exists(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "function _structuredCodeShowTree(mode,threshold,lineCount)" in content
assert "function _structuredCodeMode(" in content
assert "function _structuredCodeThreshold(" in content
def test_all_three_modes_represented(self):
"""auto / on / off must all be handled in the renderer."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
start = content.find("function _structuredCodeShowTree")
body = content[start:start + 400]
assert "'on'" in body, "mode 'on' (always Tree) must be handled"
assert "'off'" in body, "mode 'off' (always Raw) must be handled"
# 'auto' is the fallthrough; the mode reader sanitizes to it.
mode_start = content.find("function _structuredCodeMode")
mode_body = content[mode_start:mode_start + 300]
assert "'auto'" in mode_body, "mode 'auto' must be the sanitized fallback"
def test_default_threshold_fallback_is_10(self):
"""An invalid/missing threshold must fall back to 10 (original behavior)."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
thr_start = content.find("function _structuredCodeThreshold")
thr_body = content[thr_start:thr_start + 300]
assert ":10" in thr_body or "?10" in thr_body or " 10" in thr_body, (
"_structuredCodeThreshold must fall back to 10 for invalid/missing values"
)
# Sanitization bounds (1..1000) keep the value safe.
assert "1000" in thr_body, "threshold should be clamped to an upper bound"
def test_renderer_uses_helper_for_decision(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "_structuredCodeShowTree(_structuredCodeMode(),_structuredCodeThreshold(),lineCount)" in content
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_decision_helper_behaviour_via_node(self, tmp_path):
"""Evaluate the real _structuredCodeShowTree() against the prompt's cases.
- off, lineCount 999 => False
- on, lineCount 1 => True
- auto, threshold 10, count 9 => False
- auto, threshold 10, count 10 => True
- auto, invalid threshold => fallback 10 (count 10 => True, 9 => False)
"""
src = UI_JS_PATH.read_text(encoding="utf-8")
# Extract the brace-balanced body of _structuredCodeShowTree.
marker = "function _structuredCodeShowTree("
start = src.find(marker)
assert start >= 0, "_structuredCodeShowTree not found"
i = src.index("{", start)
depth = 1
i += 1
while depth > 0 and i < len(src):
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
i += 1
fn_src = src[start:i]
driver = tmp_path / "driver.js"
driver.write_text(
fn_src
+ "\n"
+ r"""
const cases = [
['off', 5, 999, false],
['on', 5, 1, true],
['auto', 10, 9, false],
['auto', 10, 10, true],
['auto', 11, 10, false],
// invalid threshold falls back to 10
['auto', NaN, 10, true],
['auto', NaN, 9, false],
// out-of-range thresholds fall back to 10 inside the pure helper
['auto', 0, 10, true],
['auto', 0, 9, false],
['auto', 99999, 10, true],
['auto', 99999, 9, false],
// unknown mode is treated as auto by the pure helper's fallthrough
['bogus', 10, 10, true],
['bogus', 10, 9, false],
];
let failures = [];
for (const [mode, thr, count, want] of cases) {
const got = _structuredCodeShowTree(mode, thr, count);
if (got !== want) failures.push(`${mode},${thr},${count} => ${got} (want ${want})`);
}
if (failures.length) { console.error('FAIL: ' + failures.join(' | ')); process.exit(1); }
console.log('OK');
""",
encoding="utf-8",
)
result = subprocess.run(
[NODE, str(driver)], capture_output=True, text=True, timeout=30
)
assert result.returncode == 0, (
f"_structuredCodeShowTree behaviour mismatch: {result.stdout}{result.stderr}"
)
class TestTreeCSS:
@@ -103,3 +242,50 @@ class TestTreeI18n:
for key in ("tree_view", "raw_view"):
count = content.count(key)
assert count >= 7, f"{key} found {count} times, expected >= 7"
def test_structured_code_setting_i18n_keys_present(self):
"""The new settings labels/options/help text must exist in i18n."""
with open("static/i18n.js", "r", encoding="utf-8") as f:
content = f.read()
for key in (
"settings_label_structured_code",
"settings_option_structured_code_auto",
"settings_option_structured_code_on",
"settings_option_structured_code_off",
"settings_label_structured_code_auto_lines",
"settings_desc_structured_code",
):
assert f"{key}:" in content, f"Missing i18n key: {key}"
class TestStructuredCodeSettingsWiring:
"""The setting must be a real, persisted WebUI setting — not a localStorage hack."""
def test_server_defaults_and_validation(self):
with open("api/config.py", "r", encoding="utf-8") as f:
content = f.read()
# Defaults preserve current behavior: auto + threshold 10.
assert '"structured_code_default_view": "auto"' in content
assert '"structured_code_auto_tree_lines": 10' in content
# Enum + range validation are registered.
assert '"structured_code_default_view": {"auto", "on", "off"}' in content
assert '"structured_code_auto_tree_lines": (1, 1000)' in content
def test_settings_ui_controls_present(self):
with open("static/index.html", "r", encoding="utf-8") as f:
content = f.read()
assert 'id="settingsStructuredCodeMode"' in content
assert 'id="settingsStructuredCodeAutoLines"' in content
def test_boot_initializes_runtime_globals(self):
with open("static/boot.js", "r", encoding="utf-8") as f:
content = f.read()
assert "window._structuredCodeDefaultView" in content
assert "window._structuredCodeAutoTreeLines" in content
def test_panel_persists_setting(self):
with open("static/panels.js", "r", encoding="utf-8") as f:
content = f.read()
assert "_structuredCodeViewFromUi" in content
assert "structured_code_default_view" in content
assert "structured_code_auto_tree_lines" in content