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 <noreply@anthropic.com>
This commit is contained in:
jenkins 2026-08-18 06:55:43 -03:00
parent 2b2c8bda14
commit f83c59a1c9
2 changed files with 49 additions and 3 deletions

View File

@ -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 {

View File

@ -93,6 +93,8 @@ const privateFilesPage = `<!doctype html>
</html>`
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});