* feat(composer): add saved prompts library with per-profile storage (#2732) * fix(composer): move saved-prompts popup out of .composer-left to preserve DOM test (#2732) * fix(composer): correct ARIA roles, add server-side prompt limits (#2732) * fix(composer): surface save-prompt errors instead of silent success toast (#2732) * Release v0.51.338 — Release LB (saved prompts library, #3571) Composer saved-prompts library (@rodboev): bookmark button → popup of saved prompts; click to insert, save current input, delete. Persists to $HERMES_HOME/webui/saved_prompts.json with server-side caps (8000 chars / 200). Maintainer work (per Nathan): conditions were (a) verify it actually works and (b) hide on mobile. Both met: - Live-verified load/save/delete all persist through the UI. - Added mobile-hide (#btnSavedPrompts,.saved-prompts-popup display:none in the @media max-width:640px + 900px composer blocks). DOM-verified visible at 1280px, vision-confirmed absent from the composer at 390px. - Added missing Polish (pl) i18n for the 5 saved_prompts_* keys (PR had en+others but not pl — failed locale-parity). - Added tests/test_issue3571_saved_prompts.py (mobile-hide + caps + wiring guards). Full suite green, ESLint/scope-undef CLEAN, Opus SHIP-safe (auth-gated, CSRF, XSS-safe, sane caps), Codex SAFE-TO-SHIP. Co-authored-by: rodboev <rodboev@users.noreply.github.com> --------- Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com>
72 lines
2.7 KiB
Python
72 lines
2.7 KiB
Python
"""#3571 saved-prompts library — structural + mobile-visibility guards.
|
|
|
|
The saved-prompts composer affordance is a desktop-only feature: per Nathan
|
|
(2026-06-09) it must be hidden on mobile (too much for the narrow composer).
|
|
These tests pin the mobile-hide rule and the core wiring so a future refactor
|
|
can't silently regress either.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def read(rel: str) -> str:
|
|
return (REPO / rel).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_saved_prompts_button_hidden_on_mobile():
|
|
"""#btnSavedPrompts (and its popup) must be display:none inside a mobile
|
|
max-width:640px @media block — desktop-only affordance. Verified by walking
|
|
each media block and confirming the hide rule lives inside a 640px-max one."""
|
|
css = read("static/style.css")
|
|
found = False
|
|
for m in re.finditer(r"@media([^{]*)\{", css):
|
|
cond = m.group(1)
|
|
if "max-width" not in cond or "640px" not in cond:
|
|
continue
|
|
# Slice to this block's matching close brace.
|
|
depth = 0
|
|
i = m.end() - 1
|
|
end = len(css)
|
|
while i < len(css):
|
|
if css[i] == "{":
|
|
depth += 1
|
|
elif css[i] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
end = i
|
|
break
|
|
i += 1
|
|
block = css[m.end():end]
|
|
if re.search(r"#btnSavedPrompts[^{]*\{[^}]*display:\s*none", block):
|
|
found = True
|
|
break
|
|
assert found, (
|
|
"#btnSavedPrompts must be hidden on mobile inside a max-width:640px block "
|
|
"(desktop-only saved-prompts feature)"
|
|
)
|
|
|
|
|
|
def test_saved_prompts_backend_caps_present():
|
|
"""The POST /api/prompts route must cap text length and total count so
|
|
saved_prompts.json can't grow unbounded."""
|
|
routes = read("api/routes.py")
|
|
assert "text too long" in routes, "POST /api/prompts must cap text length"
|
|
assert re.search(r"len\(prompts\)\s*>=\s*\d+", routes), (
|
|
"POST /api/prompts must cap the total number of saved prompts"
|
|
)
|
|
|
|
|
|
def test_saved_prompts_core_wiring_present():
|
|
"""The composer must expose the saved-prompts toggle + popup and the
|
|
load/save/delete API calls."""
|
|
js = read("static/messages.js")
|
|
assert "toggleSavedPromptsPopup" in js
|
|
assert "insertSavedPromptIntoComposer" in js
|
|
assert re.search(r"api\('/api/prompts',\s*\{method:'POST'", js), "save wiring (POST) missing"
|
|
assert re.search(r"api\('/api/prompts',\s*\{method:'DELETE'", js), "delete wiring (DELETE) missing"
|
|
html = read("static/index.html")
|
|
assert 'id="btnSavedPrompts"' in html
|
|
assert 'id="savedPromptsPopup"' in html
|