From f83c59a1c94c381f544a5cdd01bd0b6b17bf190c Mon Sep 17 00:00:00 2001 From: jenkins Date: Tue, 18 Aug 2026 06:55:43 -0300 Subject: [PATCH] hermes: declutter chat to a chat-only surface Extend the chat router HTML-injection bridge so tenants see a clean chat + conversation-history experience instead of the full agent cockpit. hideChatAdministration now hides the rail buttons and panels for logs, insights, memory, skills, workspaces, todos, tasks and profiles (alongside the existing Kanban), and bridgeCSS hides the same [data-panel] targets pre-JS to avoid a flash. Also hide the redundant legacy settings model dropdown (#settingsModel) via bridgeCSS while keeping the composer routing chip. Chat, history/sessions, new-chat, composer and the model chip are untouched. Co-Authored-By: Claude Opus 4.8 --- services/hermes/router/main_test.go | 43 ++++++++++++++++++++++++++++- services/hermes/router/web.go | 9 ++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/services/hermes/router/main_test.go b/services/hermes/router/main_test.go index 97b98347..152ebeca 100644 --- a/services/hermes/router/main_test.go +++ b/services/hermes/router/main_test.go @@ -367,11 +367,52 @@ func TestPrivateFileBrowserIsTenantAuthenticatedAndHidesKanban(t *testing.T) { assetRequest.Header.Set("X-Forwarded-User", "subject") assetResponse := httptest.NewRecorder() router.ServeHTTP(assetResponse, assetRequest) - if !strings.Contains(assetResponse.Body.String(), "endpoint('list'") || !strings.Contains(assetResponse.Body.String(), `'[data-panel="kanban"]'`) { + if !strings.Contains(assetResponse.Body.String(), "endpoint('list'") || !strings.Contains(assetResponse.Body.String(), `'[data-panel="' + panel + '"]'`) { t.Fatal("file browsing or chat-only navigation policy is missing") } } +func TestChatBridgeHidesOnlyNonChatCockpitChrome(t *testing.T) { + router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) + if err != nil { + t.Fatal(err) + } + asset := func(path string) string { + request := httptest.NewRequest(http.MethodGet, path, nil) + request.Header.Set("X-Forwarded-User", "subject") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("%s got status %d", path, response.Code) + } + return response.Body.String() + } + js := asset("/hermes-chat-bridge.js") + css := asset("/hermes-chat-bridge.css") + + // The whole cockpit (Kanban plus every non-chat rail panel) is declutter-hidden + // both pre-JS (CSS, no flash) and by the runtime .hidden reinforcement. + for _, panel := range []string{"kanban", "logs", "insights", "memory", "skills", "workspaces", "todos", "tasks", "profiles"} { + if !strings.Contains(css, `[data-panel="`+panel+`"]`) { + t.Fatalf("bridge CSS does not hide cockpit panel %q", panel) + } + if !strings.Contains(js, `'`+panel+`'`) { + t.Fatalf("bridge JS does not mark cockpit panel %q hidden", panel) + } + } + if !strings.Contains(css, "display:none") || !strings.Contains(css, "#settingsModel") { + t.Fatal("bridge CSS must hide the cockpit panels and the redundant settings model selector") + } + + // The clean chat experience (chat surface, history, composer, model chip) + // must never be part of the hide set. + for _, keep := range []string{`[data-panel="chat"]`, `[data-panel="history"]`, `[data-panel="sessions"]`, `[data-panel="composer"]`, "composerRoutingChip", "composerRoutingLabel"} { + if strings.Contains(css, keep) || strings.Contains(js, keep) { + t.Fatalf("declutter wrongly targets a core chat control: %q", keep) + } + } +} + func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { diff --git a/services/hermes/router/web.go b/services/hermes/router/web.go index 4536eb12..adb596a6 100644 --- a/services/hermes/router/web.go +++ b/services/hermes/router/web.go @@ -93,6 +93,8 @@ const privateFilesPage = ` ` const bridgeCSS = ` +[data-panel="kanban"],[data-panel="logs"],[data-panel="insights"],[data-panel="memory"],[data-panel="skills"],[data-panel="workspaces"],[data-panel="todos"],[data-panel="tasks"],[data-panel="profiles"]{display:none!important} +#settingsModel,label:has(#settingsModel),.settings-row:has(#settingsModel){display:none!important} #hermes-files-sidebar,#hermes-telegram-sidebar{display:flex;align-items:center} .hermes-link-page{margin:0;min-height:100vh;display:grid;place-items:center;background:#0f172a;color:#e2e8f0;font:16px/1.5 system-ui,sans-serif} .hermes-link-card{width:min(620px,calc(100% - 40px));box-sizing:border-box;padding:32px;border:1px solid #334155;border-radius:18px;background:#111827;box-shadow:0 20px 60px #0006} @@ -105,8 +107,11 @@ const bridgeCSS = ` const bridgeJS = `(() => { const page = document.querySelector('[data-telegram-page]'); const filesPage = document.querySelector('[data-files-page]'); + const chatAdministrationPanels = ['kanban','logs','insights','memory','skills','workspaces','todos','tasks','profiles']; const hideChatAdministration = () => { - document.querySelectorAll('[data-panel="kanban"]').forEach((node) => { node.hidden = true; }); + chatAdministrationPanels.forEach((panel) => { + document.querySelectorAll('[data-panel="' + panel + '"]').forEach((node) => { node.hidden = true; }); + }); }; hideChatAdministration(); if (!page && !filesPage) { @@ -159,7 +164,7 @@ const bridgeJS = `(() => { const observer = new MutationObserver(() => { if (scheduled) return; scheduled = true; - requestAnimationFrame(() => { scheduled = false; installSidebarLinks(); }); + requestAnimationFrame(() => { scheduled = false; hideChatAdministration(); installSidebarLinks(); }); }); installSidebarLinks(); observer.observe(document.body, {childList:true, subtree:true});