release: v0.50.243 (#1302)

release: v0.50.243

Batch release of 2 PRs.

- #1301 — fix: remove PRIMARY chip badge + add Claude Opus 4.7 label
  Drops the chip-projected configured-model badge added in #1287 (chip
  width 235px → 164px). Adds Claude Opus 4.7 label entries so the picker
  no longer renders "Claude Opus 4 7" (missing dot).
  Independently reviewed and approved by nesquena (commit c0bbd23).

- #1297 (@franksong2702) — fix: preserve cron output response snippets
  Fixes #1295. /api/crons/output now preserves the ## Response section
  when a large skill dump appears in the prompt section; falls back to
  file tail when no marker exists.

Tests: 3254 passed, 2 skipped, 3 xpassed.

Independently reviewed and approved by nesquena (commit b262e4d).
This commit is contained in:
nesquena-hermes
2026-04-29 21:06:30 -07:00
committed by GitHub
parent 20ac6dfe5c
commit ded9b7e1c4
8 changed files with 92 additions and 30 deletions
+7
View File
@@ -4,6 +4,13 @@
### Fixed
## [v0.50.243] — 2026-04-30
### Fixed
- **Chip composer model badge — removed the `PRIMARY` projection** — The chip-projected configured-model badge added in #1287 was eating ≈30% of chip width (235px → 164px) without adding signal, since the model name is already right next to it. The dropdown rows still show `Primary` / `Fallback N` badges where they actually help distinguish picker entries. Backend `_build_configured_model_badges()` and the `configured_model_badges` payload on `/api/models` are preserved for the dropdown to consume. (`static/index.html`, `static/ui.js`, `static/style.css`, `tests/test_model_picker_badges.py`) — PR #1301
- **Claude Opus 4.7 label rendering** — Adds explicit label entries for `anthropic/claude-opus-4.7`, `claude-opus-4.7`, and `claude-opus-4-7` so the picker no longer renders "Claude Opus 4 **7**" (missing dot) when the dashed-form model ID falls through to the generic dash-replace formatter. (`api/config.py`) — PR #1301
- **Cron output snippet preserves the `## Response` section**`/api/crons/output` returned `txt[:8000]` which could drop the useful response section when a large skill dump appeared in the prompt context. Now: if `## Response` exists, preserves a short header plus the response section; if no marker exists, returns the file tail rather than the head. (`api/routes.py`, `tests/test_sprint10.py`) @franksong2702 — PR #1297, fixes #1295
## [v0.50.242] — 2026-04-30
### Reverted
+3
View File
@@ -503,6 +503,7 @@ _FALLBACK_MODELS = [
{"provider": "OpenAI", "id": "openai/gpt-5.4-mini", "label": "GPT-5.4 Mini"},
{"provider": "OpenAI", "id": "openai/gpt-5.4", "label": "GPT-5.4"},
# Anthropic — 4.6 flagship + 4.5 generation
{"provider": "Anthropic", "id": "anthropic/claude-opus-4.7", "label": "Claude Opus 4.7"},
{"provider": "Anthropic", "id": "anthropic/claude-opus-4.6", "label": "Claude Opus 4.6"},
{"provider": "Anthropic", "id": "anthropic/claude-sonnet-4.6", "label": "Claude Sonnet 4.6"},
{"provider": "Anthropic", "id": "anthropic/claude-sonnet-4-5", "label": "Claude Sonnet 4.5"},
@@ -641,6 +642,7 @@ def _resolve_provider_alias(name: str) -> str:
# Well-known models per provider (used to populate dropdown for direct API providers)
_PROVIDER_MODELS = {
"anthropic": [
{"id": "claude-opus-4.7", "label": "Claude Opus 4.7"},
{"id": "claude-opus-4.6", "label": "Claude Opus 4.6"},
{"id": "claude-sonnet-4.6", "label": "Claude Sonnet 4.6"},
{"id": "claude-sonnet-4-5", "label": "Claude Sonnet 4.5"},
@@ -738,6 +740,7 @@ _PROVIDER_MODELS = {
{"id": "gpt-5", "label": "GPT-5"},
{"id": "gpt-5-codex", "label": "GPT-5 Codex"},
{"id": "gpt-5-nano", "label": "GPT-5 Nano"},
{"id": "claude-opus-4-7", "label": "Claude Opus 4.7"},
{"id": "claude-opus-4-6", "label": "Claude Opus 4.6"},
{"id": "claude-opus-4-5", "label": "Claude Opus 4.5"},
{"id": "claude-opus-4-1", "label": "Claude Opus 4.1"},
+37 -1
View File
@@ -35,6 +35,8 @@ _CLIENT_DISCONNECT_ERRORS = (
# Track job IDs currently being executed so the frontend can poll status.
_RUNNING_CRON_JOBS: dict[str, float] = {} # job_id → start_timestamp
_RUNNING_CRON_LOCK = threading.Lock()
_CRON_OUTPUT_CONTENT_LIMIT = 8000
_CRON_OUTPUT_HEADER_CONTEXT = 200
def _mark_cron_running(job_id: str):
@@ -56,6 +58,40 @@ def _is_cron_running(job_id: str) -> tuple[bool, float]:
return True, time.time() - t
def _cron_response_marker_index(text: str) -> int:
"""Return the start index of a markdown Response heading, if present."""
candidates = []
for heading in ("## Response", "# Response"):
if text.startswith(heading):
candidates.append(0)
idx = text.find(f"\n{heading}")
if idx >= 0:
candidates.append(idx + 1)
return min(candidates) if candidates else -1
def _cron_output_content_window(text: str, limit: int = _CRON_OUTPUT_CONTENT_LIMIT) -> str:
"""Return a bounded cron output window that preserves useful response text.
Cron output files can contain large skill dumps in the Prompt section. The
UI already extracts ``## Response`` when present, so keep that section in
the API payload instead of blindly returning the first ``limit`` chars.
"""
if limit <= 0:
return ""
if len(text) <= limit:
return text
response_idx = _cron_response_marker_index(text)
if response_idx >= 0:
header = text[:min(_CRON_OUTPUT_HEADER_CONTEXT, response_idx)].rstrip()
response = text[response_idx:].lstrip("\n")
content = f"{header}\n...\n{response}" if header else response
return content[:limit]
return text[-limit:]
def _run_cron_tracked(job):
"""Wrapper that tracks running state around cron.scheduler.run_job."""
try:
@@ -2932,7 +2968,7 @@ def _handle_cron_output(handler, parsed):
for f in files:
try:
txt = f.read_text(encoding="utf-8", errors="replace")
outputs.append({"filename": f.name, "content": txt[:8000]})
outputs.append({"filename": f.name, "content": _cron_output_content_window(txt)})
except Exception:
logger.debug("Failed to read cron output file %s", f)
return j(handler, {"job_id": job_id, "outputs": outputs})
-1
View File
@@ -401,7 +401,6 @@
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
<span class="composer-model-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span>
<span class="composer-model-label" id="composerModelLabel"></span>
<span class="composer-model-badge" id="composerModelBadge" hidden></span>
<span class="composer-model-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<select id="modelSelect" class="composer-model-select" title="Conversation model" aria-hidden="true" tabindex="-1">
-4
View File
@@ -824,10 +824,6 @@
.composer-model-chip:hover{color:var(--text);background-color:var(--hover-bg);}
.composer-model-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
.composer-model-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.composer-model-badge{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;padding:2px 7px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;border:1px solid transparent;}
.composer-model-badge[hidden]{display:none;}
.composer-model-badge--primary{background:rgba(50,184,198,.16);border-color:rgba(50,184,198,.32);color:#8fe7ef;}
.composer-model-badge--fallback{background:rgba(255,184,77,.14);border-color:rgba(255,184,77,.28);color:#ffd18a;}
.composer-model-icon,.composer-model-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
.composer-model-select{position:absolute!important;left:-9999px!important;width:1px!important;height:1px!important;opacity:0!important;pointer-events:none!important;}
.composer-right{display:flex;gap:8px;align-items:center;flex-shrink:0;}
-12
View File
@@ -425,28 +425,16 @@ function syncModelChip(){
const sel=$('modelSelect');
const chip=$('composerModelChip');
const label=$('composerModelLabel');
const badgeEl=$('composerModelBadge');
const dd=$('composerModelDropdown');
if(!sel||!chip||!label) return;
// Don't show a model label until boot has finished loading to prevent flash of wrong default
if(!S._bootReady){
label.textContent='';
chip.title='Conversation model';
if(badgeEl){
badgeEl.textContent='';
badgeEl.hidden=true;
badgeEl.className='composer-model-badge';
}
return;
}
const opt=_selectedModelOption();
label.textContent=opt?opt.textContent:getModelLabel(sel.value||'');
const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{});
if(badgeEl){
badgeEl.textContent=badge&&badge.label?badge.label:'';
badgeEl.hidden=!badgeEl.textContent;
badgeEl.className='composer-model-badge'+(badge&&badge.role?` composer-model-badge--${badge.role}`:'');
}
chip.title=sel.value||'Conversation model';
chip.classList.toggle('active',!!(dd&&dd.classList.contains('open')));
}
+11 -12
View File
@@ -106,18 +106,17 @@ def test_ui_renders_model_badges_from_api_payload():
"A UI precisa de um helper de matching resiliente para religar badges mesmo quando "
"o update do catálogo mudar prefixos/formas do model ID."
)
assert 'id="composerModelBadge"' in html, (
"O chip principal do modelo precisa de um container dedicado para exibir o badge "
"do modelo selecionado fora do dropdown."
# Chip-projected badge was removed in v0.50.243 (added too much width to the
# composer chip; signal value low since the model name is right next to it).
# Badges remain in the dropdown rows (model-opt-badge) for picker rows.
assert 'id="composerModelBadge"' not in html, (
"composer-model-badge chip projection was intentionally removed — "
"do not re-add it to the composer chip."
)
assert "composer-model-badge" in css, (
"O badge do chip principal precisa de estilo próprio para ficar visível ao lado "
"do nome do modelo selecionado."
assert "composer-model-badge" not in css, (
"composer-model-badge CSS was intentionally removed alongside the chip span."
)
assert "const badge=_getConfiguredModelBadge(sel.value||'',window._configuredModelBadges||{});" in js, (
"syncModelChip() deve buscar o badge configurado do modelo selecionado e projetá-lo "
"no chip principal da composer."
)
assert "badgeEl.textContent=badge&&badge.label?badge.label:'';" in js, (
"syncModelChip() deve preencher o texto do badge visível no chip principal quando houver metadata configurada."
assert "composerModelBadge" not in js, (
"syncModelChip() must not reference composerModelBadge — the chip-projected "
"badge was removed because it added too much width to the composer chip."
)
+34
View File
@@ -115,6 +115,40 @@ def test_cron_output_snippet_helper(cleanup_test_sessions):
src, _ = get_text("/static/panels.js")
assert "_cronOutputSnippet" in src
def test_cron_output_window_preserves_response_after_large_prompt(cleanup_test_sessions):
"""Large skill dumps before ## Response must not hide the useful output."""
from api.routes import _cron_output_content_window
content = (
"Job metadata\n"
"## Prompt\n"
+ ("skill dump\n" * 1200)
+ "user prompt\n"
"## Response\n"
"actual useful cron result\n"
)
window = _cron_output_content_window(content, limit=8000)
assert len(window) <= 8000
assert "## Response" in window
assert "actual useful cron result" in window
assert "Job metadata" in window
def test_cron_output_window_without_response_uses_tail(cleanup_test_sessions):
"""Without a response marker, keep the newest tail rather than old prompt text."""
from api.routes import _cron_output_content_window
content = "old prompt\n" + ("x" * 9000) + "tail result"
window = _cron_output_content_window(content, limit=8000)
assert len(window) == 8000
assert window.endswith("tail result")
assert "old prompt" not in window
# ── Tool card polish ───────────────────────────────────────────────────────
def test_tool_card_running_dot_in_css(cleanup_test_sessions):