From 1c0d13c6d9648c57aadb8092e5975a28974ad44c Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 12 Apr 2026 21:45:25 -0700 Subject: [PATCH] fix: title auto-generation + mobile close button (PR #333) + v0.50.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(merge): preserve auth errors + fix title auto-generation * fix(css): hide mobile close button on desktop for workspace panel * fix: hide duplicate collapse button in mobile workspace panel view * docs: v0.50.10 — title auto-generation fix + mobile close button (PR #333) --------- Co-authored-by: MILO Co-authored-by: Nathan Esquenazi --- CHANGELOG.md | 6 ++ api/streaming.py | 2 +- static/index.html | 2 +- static/style.css | 8 ++- tests/test_sprint41.py | 129 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/test_sprint41.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c58107f..a0801e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ --- +## [v0.50.10] Title auto-generation fix + mobile close button (PR #333) + +- **Session title now auto-generates for all default title values** (`'Untitled'`, `'New Chat'`, empty string): The condition in `api/streaming.py` that triggers `title_from()` previously only matched `'Untitled'`. It now also covers `'New Chat'` (used by some external clients/forks) and any empty/falsy title, so sessions started from those states get a proper auto-generated title after the first message. +- **Redundant workspace panel close button hidden on mobile** (`static/style.css`): On viewports ≤900px wide, both the desktop collapse button (`#btnCollapseWorkspacePanel`) and the mobile-specific X button (`.mobile-close-btn`) were rendered simultaneously. The desktop button is now hidden on mobile and `.mobile-close-btn` is hidden by default (desktop) and shown only on mobile — eliminating the duplicate control. + - 11 new tests in `tests/test_sprint41.py`; 802 tests total (up from 791) + ## [v0.50.9] Onboarding works from Docker bridge networks (PR #335, fixes #334) - **Docker users can now complete onboarding without enabling auth first** (closes #334): The onboarding setup endpoint previously only accepted requests from `127.0.0.1`. Docker containers connect via bridge network IPs (`172.17.x.x`, etc.), so the endpoint returned a 403 mid-wizard with no clear explanation. The check now accepts any loopback or RFC-1918 private address (`127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) using Python's `ipaddress.is_loopback` and `is_private`. Public IPs are still blocked unless auth is enabled. diff --git a/api/streaming.py b/api/streaming.py index 567c05c..c080794 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -335,7 +335,7 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta if isinstance(_m, dict) and not _m.get('timestamp') and not _m.get('_ts'): _m['timestamp'] = int(_now) # Only auto-generate title when still default; preserves user renames - if s.title == 'Untitled': + if s.title == 'Untitled' or s.title == 'New Chat' or not s.title: s.title = title_from(s.messages, s.title) # Read token/cost usage from the agent object (if available) input_tokens = getattr(agent, 'session_prompt_tokens', 0) or 0 diff --git a/static/index.html b/static/index.html index a786255..9eec0a4 100644 --- a/static/index.html +++ b/static/index.html @@ -526,7 +526,7 @@
System
- v0.50.9 + v0.50.10
diff --git a/static/style.css b/static/style.css index ec5b769..991b577 100644 --- a/static/style.css +++ b/static/style.css @@ -466,6 +466,7 @@ .git-badge{font-size:9px;font-weight:600;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;letter-spacing:.02em;margin-left:auto;margin-right:4px;white-space:nowrap;font-family:'SF Mono',ui-monospace,monospace;} .git-badge.dirty{color:var(--gold);background:rgba(201,168,76,.1);} .panel-actions{display:flex;gap:4px;} + .mobile-close-btn{display:none;} .panel-icon-btn{width:24px;height:24px;background:none;border:none;color:var(--muted);cursor:pointer;border-radius:5px;font-size:13px;display:flex;align-items:center;justify-content:center;transition:all .15s;} .panel-icon-btn:hover{background:rgba(255,255,255,.08);color:var(--text);} .panel-icon-btn:disabled{opacity:.35;cursor:not-allowed;} @@ -533,7 +534,12 @@ .layout.workspace-panel-collapsed .rightpanel{width:0 !important;opacity:0;transform:translateX(14px);border-left-color:transparent;pointer-events:none;} } - @media(max-width:900px){.rightpanel{display:none}.workspace-toggle-btn,.mobile-files-btn{display:inline-flex!important;}} + @media(max-width:900px){ + .rightpanel{display:none} + .workspace-toggle-btn,.mobile-files-btn{display:inline-flex!important;} + .mobile-close-btn{display:flex;} + #btnCollapseWorkspacePanel{display:none;} + } @media(max-width:640px){ /* ── Sidebar: slide-in overlay instead of hidden ── */ diff --git a/tests/test_sprint41.py b/tests/test_sprint41.py new file mode 100644 index 0000000..5e0e5f5 --- /dev/null +++ b/tests/test_sprint41.py @@ -0,0 +1,129 @@ +""" +Sprint 41 Tests: Title auto-generation fix + mobile close button CSS (PR #333). + +Covers: +- streaming.py: sessions titled 'New Chat' trigger auto-title generation +- streaming.py: sessions with empty/falsy title trigger auto-title generation +- streaming.py: sessions titled 'Untitled' (original guard) still trigger +- streaming.py: sessions with a user-set title do NOT trigger auto-title +- style.css: .mobile-close-btn is hidden by default (desktop rule present) +- style.css: .mobile-close-btn shown in <=900px media query +- style.css: #btnCollapseWorkspacePanel hidden in <=900px media query +- index.html: both .mobile-close-btn and #btnCollapseWorkspacePanel buttons exist +""" +import pathlib +import re +import unittest + +REPO_ROOT = pathlib.Path(__file__).parent.parent +CSS = (REPO_ROOT / "static" / "style.css").read_text() +HTML = (REPO_ROOT / "static" / "index.html").read_text() +STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text() + + +# ── streaming.py: title auto-generation condition ───────────────────────── + +class TestTitleAutoGenerationCondition(unittest.TestCase): + """Verify the guarded condition in streaming.py covers all default title cases.""" + + def _titles_that_trigger(self): + """Extract the condition from the source so tests stay in sync with code.""" + # Find the if-condition that calls title_from + m = re.search( + r'if\s+(s\.title\s*==.*?):\s*\n\s*s\.title\s*=\s*title_from', + STREAMING_PY, + re.DOTALL, + ) + self.assertIsNotNone(m, "Could not find title auto-generation condition in streaming.py") + return m.group(1) + + def test_untitled_in_condition(self): + cond = self._titles_that_trigger() + self.assertIn("'Untitled'", cond, "Original 'Untitled' guard must be present") + + def test_new_chat_in_condition(self): + cond = self._titles_that_trigger() + self.assertIn("'New Chat'", cond, "'New Chat' guard must be present (PR #333)") + + def test_empty_title_guard_in_condition(self): + cond = self._titles_that_trigger() + self.assertIn("not s.title", cond, "Empty/falsy title guard must be present (PR #333)") + + def test_condition_logic_covers_all_defaults(self): + """The condition uses OR so any one default title triggers generation.""" + cond = self._titles_that_trigger() + # All three guards must be joined by 'or' + parts = re.split(r'\bor\b', cond) + self.assertGreaterEqual(len(parts), 3, + "Expected at least 3 OR-joined sub-conditions (Untitled, New Chat, not s.title)") + + +# ── style.css: mobile close button visibility ───────────────────────────── + +class TestMobileCloseButtonCSS(unittest.TestCase): + """Verify CSS rules that control the duplicate close button on mobile.""" + + def test_mobile_close_btn_hidden_by_default(self): + """Desktop default: .mobile-close-btn must be display:none outside any media query.""" + # Find the rule before the first @media block that contains mobile-close-btn + # We look for the pattern in the desktop (non-media-query) section + self.assertIn( + ".mobile-close-btn{display:none;}", + CSS.replace(" ", ""), + ".mobile-close-btn should be hidden by default (desktop) — rule missing or wrong" + ) + + def test_mobile_close_btn_shown_in_900px_query(self): + """Inside max-width:900px media query, .mobile-close-btn must be display:flex.""" + # Extract the 900px media block + m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', + CSS) + self.assertIsNotNone(m, "@media(max-width:900px) block not found in style.css") + block = m.group(1).replace(" ", "") + self.assertIn(".mobile-close-btn{display:flex;}", + block, + ".mobile-close-btn must be display:flex inside the 900px media query") + + def test_desktop_collapse_btn_hidden_in_900px_query(self): + """Inside max-width:900px media query, #btnCollapseWorkspacePanel must be display:none.""" + m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', + CSS) + self.assertIsNotNone(m, "@media(max-width:900px) block not found in style.css") + block = m.group(1).replace(" ", "") + self.assertIn("#btnCollapseWorkspacePanel{display:none;}", + block, + "#btnCollapseWorkspacePanel must be display:none in 900px media query") + + def test_900px_query_retains_existing_rules(self): + """Ensure the PR didn't accidentally drop existing rules from the 900px block.""" + m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', + CSS) + self.assertIsNotNone(m) + block = m.group(1) + self.assertIn("rightpanel", block, ".rightpanel rule missing from 900px block") + self.assertIn("mobile-files-btn", block, ".mobile-files-btn rule missing from 900px block") + + +# ── index.html: button presence ─────────────────────────────────────────── + +class TestWorkspacePanelButtons(unittest.TestCase): + """Verify both panel buttons are present in the HTML so CSS rules have targets.""" + + def test_desktop_collapse_button_exists(self): + self.assertIn("btnCollapseWorkspacePanel", HTML, + "#btnCollapseWorkspacePanel button must exist in index.html") + + def test_mobile_close_button_exists(self): + self.assertIn("mobile-close-btn", HTML, + ".mobile-close-btn button must exist in index.html") + + def test_mobile_close_button_has_aria_label(self): + """Accessibility: mobile close button must have an aria-label.""" + m = re.search(r'class="[^"]*mobile-close-btn[^"]*"[^>]*>', HTML) + self.assertIsNotNone(m, "Could not find mobile-close-btn element") + self.assertIn("aria-label", m.group(0), + "mobile-close-btn must have aria-label for accessibility") + + +if __name__ == "__main__": + unittest.main()