diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 2e385eec91..a4ee993db6 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -879,6 +879,36 @@ def test_config_set_statusbar_survives_non_dict_display(tmp_path, monkeypatch): assert saved["display"]["tui_statusbar"] == "bottom" +def test_config_set_details_mode_pins_all_sections(tmp_path, monkeypatch): + import yaml + + cfg_path = tmp_path / "config.yaml" + cfg_path.write_text( + yaml.safe_dump( + {"display": {"sections": {"tools": "expanded", "activity": "hidden"}}} + ) + ) + monkeypatch.setattr(server, "_hermes_home", tmp_path) + + resp = server.handle_request( + { + "id": "1", + "method": "config.set", + "params": {"key": "details_mode", "value": "collapsed"}, + } + ) + + assert resp["result"] == {"key": "details_mode", "value": "collapsed"} + saved = yaml.safe_load(cfg_path.read_text()) + assert saved["display"]["details_mode"] == "collapsed" + assert saved["display"]["sections"] == { + "thinking": "collapsed", + "tools": "collapsed", + "subagents": "collapsed", + "activity": "collapsed", + } + + def test_config_set_section_writes_per_section_override(tmp_path, monkeypatch): import yaml diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 24f6baf718..7eae9e7f99 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -128,6 +128,8 @@ _cfg_path = None _SLASH_WORKER_TIMEOUT_S = max( 5.0, float(os.environ.get("HERMES_TUI_SLASH_TIMEOUT_S", "45") or 45) ) +_DETAIL_SECTION_NAMES = ("thinking", "tools", "subagents", "activity") +_DETAIL_MODES = frozenset({"hidden", "collapsed", "expanded"}) # ── Async RPC dispatch (#12546) ────────────────────────────────────── # A handful of handlers block the dispatcher loop in entry.py for seconds @@ -3149,19 +3151,26 @@ def _(rid, params: dict) -> dict: if key == "details_mode": nv = str(value or "").strip().lower() - allowed_dm = frozenset({"hidden", "collapsed", "expanded"}) - if nv not in allowed_dm: + if nv not in _DETAIL_MODES: return _err(rid, 4002, f"unknown details_mode: {value}") - _write_config_key("display.details_mode", nv) + cfg = _load_cfg() + display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {} + sections = display.get("sections") if isinstance(display.get("sections"), dict) else {} + display["details_mode"] = nv + for section in _DETAIL_SECTION_NAMES: + sections[section] = nv + display["sections"] = sections + cfg["display"] = display + _save_cfg(cfg) return _ok(rid, {"key": key, "value": nv}) if key.startswith("details_mode."): # Per-section override: `details_mode.
` writes to - # `display.sections.
`. Empty value clears the override - # and lets the section fall back to the global details_mode. + # `display.sections.
`. Empty value clears the explicit + # override and lets frontend resolution apply built-in section defaults + # before the global details_mode. section = key.split(".", 1)[1] - allowed_sections = frozenset({"thinking", "tools", "subagents", "activity"}) - if section not in allowed_sections: + if section not in _DETAIL_SECTION_NAMES: return _err(rid, 4002, f"unknown section: {section}") cfg = _load_cfg() @@ -3178,8 +3187,7 @@ def _(rid, params: dict) -> dict: _save_cfg(cfg) return _ok(rid, {"key": key, "value": ""}) - allowed_dm = frozenset({"hidden", "collapsed", "expanded"}) - if nv not in allowed_dm: + if nv not in _DETAIL_MODES: return _err(rid, 4002, f"unknown details_mode: {value}") sections_cfg[section] = nv diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 56128e388f..e8c50c05d2 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -180,6 +180,12 @@ describe('createSlashHandler', () => { expect(createSlashHandler(ctx)('/details toggle')).toBe(true) expect(getUiState().detailsMode).toBe('expanded') expect(getUiState().detailsModeCommandOverride).toBe(true) + expect(getUiState().sections).toEqual({ + thinking: 'expanded', + tools: 'expanded', + subagents: 'expanded', + activity: 'expanded' + }) expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'details_mode', value: 'expanded' diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 1b29366361..f9b54c34c1 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -266,7 +266,9 @@ export const coreCommands: SlashCommand[] = [ return transcript.sys(DETAILS_USAGE) } - patchUiState({ detailsMode: next, detailsModeCommandOverride: true }) + const sections = Object.fromEntries(SECTION_NAMES.map(section => [section, next])) + + patchUiState({ detailsMode: next, detailsModeCommandOverride: true, sections }) gateway.rpc('config.set', { key: 'details_mode', value: next }).catch(() => {}) transcript.sys(`details: ${next}`) }