fix: title auto-generation + mobile close button (PR #333) + v0.50.10
* 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 <milo@MILOdeMacMINI-2.local> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
@@ -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)
|
## [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.
|
- **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.
|
||||||
|
|||||||
@@ -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'):
|
if isinstance(_m, dict) and not _m.get('timestamp') and not _m.get('_ts'):
|
||||||
_m['timestamp'] = int(_now)
|
_m['timestamp'] = int(_now)
|
||||||
# Only auto-generate title when still default; preserves user renames
|
# 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)
|
s.title = title_from(s.messages, s.title)
|
||||||
# Read token/cost usage from the agent object (if available)
|
# Read token/cost usage from the agent object (if available)
|
||||||
input_tokens = getattr(agent, 'session_prompt_tokens', 0) or 0
|
input_tokens = getattr(agent, 'session_prompt_tokens', 0) or 0
|
||||||
|
|||||||
@@ -526,7 +526,7 @@
|
|||||||
<div class="settings-section-title">System</div>
|
<div class="settings-section-title">System</div>
|
||||||
<div class="settings-section-meta">Instance version and access controls.</div>
|
<div class="settings-section-meta">Instance version and access controls.</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="settings-version-badge">v0.50.9</span>
|
<span class="settings-version-badge">v0.50.10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||||
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
||||||
|
|||||||
@@ -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{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);}
|
.git-badge.dirty{color:var(--gold);background:rgba(201,168,76,.1);}
|
||||||
.panel-actions{display:flex;gap:4px;}
|
.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{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:hover{background:rgba(255,255,255,.08);color:var(--text);}
|
||||||
.panel-icon-btn:disabled{opacity:.35;cursor:not-allowed;}
|
.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;}
|
.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){
|
@media(max-width:640px){
|
||||||
/* ── Sidebar: slide-in overlay instead of hidden ── */
|
/* ── Sidebar: slide-in overlay instead of hidden ── */
|
||||||
|
|||||||
129
tests/test_sprint41.py
Normal file
129
tests/test_sprint41.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user