Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion api/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ def on_tool(name, preview, args):
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
Expand Down
2 changes: 1 addition & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ <h3 style="margin:0;font-size:18px">Control Center</h3>
<div class="settings-section-title">System</div>
<div class="settings-section-meta">Instance version and access controls.</div>
</div>
<span class="settings-version-badge">v0.50.9</span>
<span class="settings-version-badge">v0.50.10</span>
</div>
<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>
Expand Down
8 changes: 7 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
Expand Down Expand Up @@ -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 ── */
Expand Down
129 changes: 129 additions & 0 deletions tests/test_sprint41.py
Original file line number Diff line number Diff line change
@@ -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()
Loading