Skip to content

Commit 2de5d37

Browse files
Smrutiranjan Patriclaude
andcommitted
feat(tui): lovability pass - argument+sources on completion card, epigraph, actionable errors
Completion card now leads with the argument the piece made (thesis claim) + a source tally (count, high-influence, high-authority) + reading time, via a new polish.source_stats() shared with the evidence report. The welcome shows a rotating, date-stable public-domain writing epigraph with a 'Welcome back.' lead for returning writers (one line; compactness guard 14->15). ui.explain_error() now maps context-window overflow and token-budget failures to actionable next steps instead of a raw traceback. Existing _summary_card/_paused_card were already strong; this fills the gaps rather than duplicating them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5da6436 commit 2de5d37

7 files changed

Lines changed: 88 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1717
New files: `personas/{wildean,poe-gothic,dickensian,whitmanesque}.md`.
1818
- **Welcome footer shows agentic state.** The status footer now reads `agentic on|off` (replacing the
1919
redundant `flash` model slot, which duplicated `pro`), and the hint line surfaces `/agentic on|off`.
20+
- **Lovability pass on the TUI.** The completion card now leads with **the argument the piece made**
21+
(its thesis claim) plus a **source tally** (N sources · high-influence · high-authority) and
22+
**reading time** — "proof, not vibes" the moment a run finishes (new `polish.source_stats()`, shared
23+
with the evidence report so they never disagree). The welcome shows a rotating, date-stable **writing
24+
epigraph** (public-domain voices only), prefixed with **"Welcome back."** for a returning writer, on a
25+
single line. And `ui.explain_error()` now maps **context-window overflow** and **token-budget**
26+
failures to actionable next steps (`/set max_context_chars …` / `/set max_run_tokens 0`) instead of a
27+
raw traceback.
2028

2129
### Changed
2230
- **Cache-friendly prompt ordering (token efficiency, no quality cost).** The writer + critic prompts

resume.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
config requests run from plain English ("turn on researcher", "set chapters to 12", "use the
3232
poe-gothic persona"). Only `/user` + `delete` stay manual. Fixed a latent bug where the prompt told
3333
the model to emit `/set` while the extractor dropped it. Test updated (`test_hardening.py`).
34+
- **TUI lovability pass.** Audited the shell for delight and found the base is already strong
35+
(`_summary_card`, `_paused_card` exist + are wired). Implemented the genuine gaps: the completion
36+
card now leads with the **argument made** + **source tally** + **reading time** (new
37+
`polish.source_stats()`, shared with the evidence report); the welcome shows a rotating, date-stable
38+
**writing epigraph** + a **"Welcome back."** lead for returning writers (one line; compactness guard
39+
bumped 14→15); `ui.explain_error()` maps **context-overflow** and **budget** failures to actionable
40+
fixes. Skipped a redundant `/why` (eval/tableread/summary already show the work).
3441
- **Test hermeticity.** Added an autouse `_isolated_settings` fixture (`tests/conftest.py`) pointing
3542
`config._SETTINGS` at a tmp path, so the suite always runs against shipped dataclass defaults and a
3643
developer's personal `settings.yaml` (e.g. `agentic=true`) can't turn the local run red. CI already

src/writingagent/polish.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,27 @@ def build_evidence_report(manuscript_md: str, thesis_md: str = "", title: str =
351351
return "\n".join(out).rstrip() + "\n"
352352

353353

354+
def source_stats(manuscript_md: str) -> dict:
355+
"""A quick, deterministic tally of the finished piece's References (no model call):
356+
``{count, high_influence, credible, avg_auth}``. Shares the row parsing + authority
357+
scoring with ``build_evidence_report`` so the completion card and the evidence report
358+
can never disagree. ``count == 0`` when there is no ranked References block yet."""
359+
rows = _REF_ROW.findall(manuscript_md or "")
360+
if not rows:
361+
return {"count": 0, "high_influence": 0, "credible": 0, "avg_auth": 0}
362+
scores = [int(s) for s, _d, _t in rows]
363+
auths = []
364+
for _s, _d, tail in rows:
365+
um = _ROW_URL.search(tail)
366+
auths.append(source_authority(um.group(1) if um else ""))
367+
return {
368+
"count": len(rows),
369+
"high_influence": sum(1 for s in scores if s >= 50),
370+
"credible": sum(1 for a in auths if a >= AUTH_REPUTABLE),
371+
"avg_auth": round(sum(auths) / len(auths)) if auths else AUTH_NEUTRAL,
372+
}
373+
374+
354375
# ── cross-chapter cohesion (book, D-008) ─────────────────────────────────────────
355376
def _prose_only(md: str) -> str:
356377
"""Strip fenced code and headings so repetition scanning sees only prose."""

src/writingagent/shell/branding.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,25 @@ def _book_status_rows(uid: str, projects: list[tuple[str, str]]) -> list[tuple[s
398398
return rows
399399

400400

401+
# A rotating writing epigraph for the welcome - public-domain voices only (consistent with the
402+
# personas' no-living-author rule). Kept short so it never wraps; chosen by the date so it's
403+
# stable within a day, varied across days.
404+
_EPIGRAPHS = [
405+
("Easy reading is damn hard writing.", "Nathaniel Hawthorne"),
406+
("Omit needless words.", "William Strunk Jr."),
407+
("Vigorous writing is concise.", "William Strunk Jr."),
408+
("The pen is the tongue of the mind.", "Cervantes"),
409+
("The secret of being a bore is to tell everything.", "Voltaire"),
410+
("What is written without effort is read without pleasure.", "Samuel Johnson"),
411+
("Substitute 'damn' for every 'very'; your editor will delete it.", "Mark Twain"),
412+
]
413+
414+
415+
def _epigraph() -> tuple[str, str]:
416+
import datetime
417+
return _EPIGRAPHS[datetime.date.today().toordinal() % len(_EPIGRAPHS)]
418+
419+
401420
def _welcome(console, cfg: ModelConfig, settings: Settings, uid: str) -> None:
402421
sdir = brain.skills_dir(uid)
403422
skl = sorted(p.stem for p in sdir.glob("*.md")) if sdir.exists() else []
@@ -433,6 +452,12 @@ def _welcome(console, cfg: ModelConfig, settings: Settings, uid: str) -> None:
433452
if fake:
434453
console.print(Text(f" {fake_msg}", style=f"bold {ERR}"))
435454

455+
# A single warm line: a rotating writing epigraph, prefixed with a greeting for a
456+
# returning writer. One line by design (see the compactness note above).
457+
quote, who = _epigraph()
458+
lead = "Welcome back. " if projects else ""
459+
console.print(Text(f" {lead}{quote}” — {who}", style=f"italic {DIM}"))
460+
436461
# ── No API key yet: point at the one command that fixes it (the wizard ran first). ─
437462
if _provider_needs_key(settings):
438463
p = _active_provider(settings)

src/writingagent/shell/dashboard.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,21 +340,38 @@ def _summary_card(console, dash, state: dict, uid: str, book_id: str) -> None:
340340
from rich.panel import Panel
341341
from rich.text import Text
342342

343-
from .. import llm
343+
from .. import llm, polish
344344
from ..brain import ArticlePaths, BookPaths
345345
console.print() # settle: the Live's last frame has no trailing newline, so the
346346
# summary Panel border would otherwise glue onto the last log line
347347
is_article = state.get("mode") == "article"
348348
paths = ArticlePaths(book_id, uid) if is_article else BookPaths(book_id, uid)
349-
words = len((brain.read_text(paths.manuscript) or "").split())
349+
manuscript = brain.read_text(paths.manuscript) or ""
350+
words = len(manuscript.split())
351+
read_min = polish.read_time_min(manuscript)
350352
insights = [i for i in (state.get("insights") or []) if isinstance(i, int)]
351353
units = state.get("num_sections" if is_article else "num_chapters", "?")
352354
toks, cost = llm.current_tokens(), llm.current_cost()
353355

354356
body = Text()
355357
body.append(f"{units} {'sections' if is_article else 'chapters'}", style=PARCH)
356358
body.append(f" · {words:,} words", style=PARCH)
359+
if read_min:
360+
body.append(f" · {read_min} min read", style=DIM)
357361
body.append(f" · {dash._elapsed()} elapsed\n", style=DIM)
362+
# Proof, not vibes: the argument it made + how grounded it is, right at the top.
363+
if dash.brief:
364+
body.append("argued: ", style=DIM)
365+
claim = dash.brief if len(dash.brief) <= 150 else dash.brief[:149].rstrip() + "…"
366+
body.append(f"{claim}\n", style=f"italic {PARCH}")
367+
stats = polish.source_stats(manuscript)
368+
if stats["count"]:
369+
body.append(f"sourced: {stats['count']} sources", style=DIM)
370+
if stats["high_influence"]:
371+
body.append(f" · {stats['high_influence']} high-influence", style=DIM)
372+
if stats["credible"]:
373+
body.append(f" · {stats['credible']} high-authority", style=DIM)
374+
body.append("\n", style=DIM)
358375
body.append(f"{toks:,} tokens", style=DIM)
359376
if cost > 0:
360377
body.append(f" · ${cost:.4f}", style=f"bold {GOLD}")

src/writingagent/ui.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,13 @@ def explain_error(exc) -> str | None:
278278
return "Network/provider hiccup — check your connection, then run again (saved & resumable)."
279279
if any(k in s for k in ("permission", "another process", "being used", "in use", "locked")):
280280
return "A file is locked (open in another program?) — close it, then try again."
281+
if any(k in s for k in ("context length", "context_length", "maximum context", "context window",
282+
"too many tokens", "reduce the length", "string too long")):
283+
return ("Prompt outgrew the model's context window — try a smaller context "
284+
"(`/set max_context_chars 16000`) or split into more, shorter units; progress is saved.")
285+
if any(k in s for k in ("budget", "max_run_tokens", "token budget")):
286+
return ("Run token budget reached — lift it with `/set max_run_tokens 0` (0 = unlimited), "
287+
"then run again (everything committed is saved).")
281288
return None
282289

283290

tests/test_ui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def test_welcome_is_compact(tmp_brain, monkeypatch):
127127
from writingagent.config import load_config, load_settings
128128
console = _record_console()
129129
shell._welcome(console, load_config(), load_settings(), "u")
130-
assert len(console.file.getvalue().splitlines()) <= 14
130+
assert len(console.file.getvalue().splitlines()) <= 15 # +1 for the one-line epigraph
131131

132132

133133
def test_welcome_warns_on_fake_mode(tmp_brain, monkeypatch):

0 commit comments

Comments
 (0)