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});