feat(ui): add necromancer score metric (fixes #45) - #280
Conversation
|
Tip 👋 Hey @Diwakar-odds — Miku's on it. Here are the most useful commands for this PR:
Note 🎓 This repo is part of ECSOC26. Run Note ⭐ Star this repo to unlock all commands. Say 📖 All commands🤖 AI-powered — 14 commands
🔧 Issue & PR management — 18 commands
🎉 Community & utility — 9 commands
|
PR Analysis & Label RequestThis PR introduces the Necromancer Score (Issue #45) to the TUI dashboards. It calculates deep historical patterns (resurrecting 6-month dormant projects) and seamlessly integrates these visual metrics across all three main dashboard components. ECSoC26 Label Justification:
Please review when you have a moment. Thank you! |
|
| Filename | Overview |
|---|---|
| termstory/tui.py | Adds necromancer header rendering, but the wrapped header still references an undefined commit count. |
| tests/test_tui.py | Updates TUI tests for deterministic avatar output and more reliable async UI interactions. |
Reviews (4): Last reviewed commit: "fix: resolve tui.py conflicts and header..." | Re-trigger Greptile
| days_diff = (session_dt - last_seen_dt).days | ||
|
|
||
| # Check for resurrection: > 180 days dead + meaningful session | ||
| if days_diff > 180: |
There was a problem hiding this comment.
Exact Threshold Revivals Dropped
When a project has been inactive for exactly 180 days, this strict check returns no resurrection even though the feature is described as counting 180+ day dormancy. The TUI then shows a necromancer score that is too low for that boundary case, while the related project necromancer calculation treats the same threshold inclusively.
| if days_diff > 180: | |
| if days_diff >= 180: |
bitflicker64
left a comment
There was a problem hiding this comment.
Hey @Diwakar-odds, picking up #45 and #38 back-to-back is good momentum. But this PR has the same three blockers as #282 and a few of its own, so it needs another pass before merge.
What's blocking
-
CI is red on all four Python versions (
MarkupError: auto closing tag ('[/]') has nothing to closeintest_tui_landing_page_after_onboarding). The cause is a markup bug in the new header line, not a logic bug: the f-string attermstory/tui.py:934ends with...[/dim][/]— that's one extra[/]because the wrapping[/]from the previous pattern was retained. Compare to the line it replaces (PROJECTS:row) which balances cleanly. Same bug class as the DAILY CLASS header in #282. Fix: drop the trailing[/], or drop the leading[bold cyan]from theNECROMANCER:segment if you intended the structure from #282. Three sites have the bug (lines 934, 1167, 1366). -
Duplicate logic with
calculate_project_necromancer_score. Acalculate_project_necromancer_score(sessions, projects) -> Dict[str, Any]already exists onmainattermstory/insights.py:578(line 621 on the PR head). It's wired intoformatter.pyviaformat_necromancer_scoreand used incli.py. The PR adds a near-duplicate under a slightly different name (calculate_necromancer_score, singular "project" dropped, noprojectsparameter, returnsint). Two implementations of the same metric will drift. Greptile flagged a related concern but didn't surface the duplicate itself. -
The two implementations disagree on the threshold. This new helper uses
days_diff > 180(strict, off-by-one for exactly 180 days, which is what the issue describes). The existingcalculate_project_necromancer_scoreusesgap >= 180 * 24 * 3600(inclusive). Pick one and make the codebase consistent — preferably use the existing helper since it already has tests and a formatter.
Suggested fix shape
- Delete this new
calculate_necromancer_score. Reuse the existingcalculate_project_necromancer_scorefrom the formatter path (.get("score", 0)) in the three header sites instead. - Fix the trailing
[/]in all three header lines. - If you want the "meaningful session" filter (>5 mins OR >10 commands) on top of the existing helper, add it as a post-filter in the formatter or wrap it in a thin helper that calls the existing one — don't fork the implementation.
Smaller notes
- New helper doesn't filter
is_legacysessions; the existing one does. Same drift concern. last_seen_by_project[s.project_id] = datetime.fromtimestamp(end_ts)updates even for sessions you skip (e.g. None project_id above), but that's fine here because thecontinuehappens before the update. Worth a comment for the next reader.- Wrapped header at
tui.py:1167replacesCOMMITS:withNECROMANCER:—total_commitsis still computed but no longer rendered. Same regression as #282. Add a row, don't replace. NECROMANCER:is 11 chars while most other labels in the column are 9 (PROJECTS:,COMMITS:). The extra spaces after the label compensate, but the row will look misaligned if you ever shorten the label. Minor.- No tests for the new helper. At minimum add a unit test that constructs a session with a 180-day gap and asserts the score.
Happy to approve once the duplicate is removed (or the two are reconciled), the markup is fixed, and CI is green.
| break | ||
| return streak | ||
|
|
||
| def calculate_necromancer_score(sessions: List[Session]) -> int: |
There was a problem hiding this comment.
Duplicate of calculate_project_necromancer_score at termstory/insights.py:621 on this branch. The existing one is already wired into formatter.py:format_necromancer_score and cli.py, has tests in tests/test_insights.py, and returns the dict shape the formatter expects. Two implementations of the same metric will drift — recommended path is to delete this helper and pull .get("score", 0) from the existing one in the three header sites.
| for s in sorted_sessions: | ||
| if s.project_id is None: | ||
| continue | ||
|
|
There was a problem hiding this comment.
Threshold off-by-one. Issue #45 says "6+ months"; this strict > 180 excludes exactly-180-day revivals. The existing calculate_project_necromancer_score uses gap >= 180 * 24 * 3600 (inclusive) and matches the issue text. Reconcile to >= 180, or just reuse the existing helper and drop this one entirely.
| header_lines.append(f"[bold cyan]{avatar_lines[7]}[/] [bold cyan]FOCUS SCORE:[/] [bold green]{fs:.1f}/10.0[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[8]}[/] [bold cyan]PEAK VELOCITY:[/] [dim]{peak_velocity}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]PROJECTS:[/] [dim]{active_projects_count}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim][/]") |
There was a problem hiding this comment.
Root cause of the CI failure (MarkupError: auto closing tag ('[/]') has nothing to close on all 4 Python versions). The f-string ends with ...[/dim][/] — that's an extra [/] because the wrapping [/] from the previous pattern was kept. The line it replaces (PROJECTS:[/] ... [dim]{active_projects_count}[/]) balances cleanly with one [/] at the end. Fix: drop the trailing [/], leaving [dim]{necro_score} dead projects revived[/dim].
| header_lines.append(f"[bold cyan]{avatar_lines[7]}[/] [bold cyan]FOCUS SCORE:[/] [bold green]{fs:.1f}/10.0[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[8]}[/] [bold cyan]PEAK VELOCITY:[/] [dim]{peak_velocity}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]COMMITS:[/] [dim]{total_commits}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim][/]") |
There was a problem hiding this comment.
Same extra [/] markup bug. Fix mirrors line 934.
| header_lines.append(f"[bold cyan]{avatar_lines[7]}[/] [bold cyan]FOCUS SCORE:[/] [bold green]{fs:.1f}/10.0[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[8]}[/] [bold cyan]PEAK TIME:[/] [dim]{peak_velocity}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]PROJECTS:[/] [dim]{len(projects)}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim][/]") |
There was a problem hiding this comment.
Same extra [/] markup bug. Fix mirrors line 934.
| header_lines.append(f"[bold cyan]{avatar_lines[7]}[/] [bold cyan]FOCUS SCORE:[/] [bold green]{fs:.1f}/10.0[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[8]}[/] [bold cyan]PEAK VELOCITY:[/] [dim]{peak_velocity}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]COMMITS:[/] [dim]{total_commits}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim][/]") |
There was a problem hiding this comment.
Same regression as PR #282 — wrapped view loses total_commits because the header row is replaced instead of added. total_commits is still computed above this line. Add a second header_lines.append instead of swapping.
|
I've resolved the test suite failures and the UI rendering bugs (the missing closing tags that swallowed the error). The tests for the Necromancer score and the daily/monthly wrapped views now pass consistently. Let me know if further changes are needed! |
|
Thanks for the follow-up. I re-reviewed the current head ( pytest tests/test_insights.py::test_calculate_project_necromancer_score \
tests/test_tui.py::test_tui_landing_page_after_onboarding \
tests/test_tui.py::test_wrapped_view_generation_and_layout -qThat subset passes (
Suggested fix shape: remove the unused helper, compute |
bitflicker64
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Request changes — CI-breaking type misuse + large unrelated TUI churn; reuses the wrong return shape for an existing metric.
Intent
Surface a “necromancer” (dead-project revival) score in TUI headers for issue #45.
Critical
-
necro_scoreis a dict, interpolated into Rich markup
calculate_project_necromancer_score(...)returns{"score": int, "resurrections": [...]}. Embedding{necro_score}stringifies that dict; list brackets in the string are interpreted as Rich tags →MarkupError: auto closing tag ('[/]') has nothing to close(matches the known failure mode for this suite).Use the integer:
necro = calculate_project_necromancer_score(sessions, projects) necro_score = necro["score"]
-
Dead / duplicate helper — this PR also adds unused
calculate_necromancer_scoreininsights.pywhile main already has the richercalculate_project_necromancer_score. Delete the new int-only duplicate; call the existing API.
Warnings
- Large rewrite of
render_time_summary(try-wrap, removegenerate_daily_chronicleimport, discard expressions likelen({s.date_str...})andsum(len(s.commits)...)) is unrelated noise for a metric PR — shrink the diff. - Header replaces a row instead of adding capacity → loses PROJECTS / COMMITS in some views (same class of regression as #281/#282). Prefer adding a row or a dedicated stats line.
- Threshold/semantics differ between the unused helper (
> 180days + “meaningful session”) and the existing function (gap >= 180 days, no meaningfulness filter). Pick one definition (issue #45) and stick to the shared helper. - No focused unit tests for the score itself (only TUI churn).
Suggestions
- Wire
necro["score"]only; keep diff to insights (if needed) + 3 header call sites + small pure tests. - Don’t invent a second necromancer implementation.
Verdict
Request changes — fix dict-in-markup crash and drop the duplicate helper before re-review.
| header_lines.append(f"[bold cyan]{avatar_lines[6]}[/] [bold cyan]ACTIVE REPOS:[/] [bold]{active_projects_count} Workspaces[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[7]}[/] [bold cyan]FOCUS SCORE:[/] [bold green]{fs:.1f}/10.0[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[8]}[/] [bold cyan]PEAK VELOCITY:[/] [dim]{peak_velocity}[/]") | ||
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim]") |
There was a problem hiding this comment.
Critical: necro_score is the full dict from calculate_project_necromancer_score. str(dict) can include [...] which Rich parses as markup and blows up TUI tests.
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score} dead projects revived[/dim]") | |
| header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]NECROMANCER:[/] [dim]{necro_score['score']} dead projects revived[/dim]") |
(Or better: assign necro_score = calculate_project_necromancer_score(... )["score"] once above and keep {necro_score}.) Same fix needed at the other two header sites.
| break | ||
| return streak | ||
|
|
||
| def calculate_necromancer_score(sessions: List[Session]) -> int: |
There was a problem hiding this comment.
Blocking: This new calculate_necromancer_score is unused and duplicates (with different thresholds/semantics) the existing calculate_project_necromancer_score later in this file. Delete it and call the existing helper from the TUI.
| proj_name = "Other" | ||
| project_seconds[proj_name] += s.duration_seconds | ||
| len({s.date_str for s in sessions if s.start_time}) | ||
|
|
There was a problem hiding this comment.
This expression result is discarded (len({...}) with no assignment). Looks like leftover from the large re-indent. Please drop dead code and keep the PR scoped to the metric.
|
/triage |
|
miku ✅ Marked as triaged ( |
|
:miku /check-star |
|
miku @Diwakar-odds still hasn't starred this repo. Applied Star this repo first, then run |
|
👋 Thanks for the PR, @Diwakar-odds! To get this PR reviewed, please ⭐ star this repo — it's free and helps others discover it. Once starred, run :miku /check-star on this PR and I'll start helping out. A |
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
|
👋 Thanks for the PR, @Diwakar-odds! To get this PR reviewed, please ⭐ star this repo — it's free and helps others discover it. Once starred, run :miku /check-star on this PR and I'll start helping out. A |
|
I have addressed all the feedback points:
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe TUI now calculates and displays Project Necromancer scores in summary, wrapped, and daily chronicle views. Rendering errors are logged and re-raised, telemetry calculations are adjusted, and asynchronous tests poll for controls while avoiding persistence side effects. ChangesNecromancer TUI telemetry
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant DetailsCanvas
participant NecromancerScoring
participant SummaryView
DetailsCanvas->>NecromancerScoring: calculate project Necromancer score
NecromancerScoring-->>DetailsCanvas: return score data
DetailsCanvas->>SummaryView: render score and summary lines
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| peak_velocity = "late night grinds" | ||
|
|
||
| total_commits = sum(len(s.commits) for s in sessions) | ||
| sum(len(s.commits) for s in sessions) |
There was a problem hiding this comment.
Commit Count Missing This line computes the wrapped-view commit count but does not store it. The header still reads
total_commits a few lines later, so opening a wrapped view raises NameError before the header and action buttons finish rendering.
| sum(len(s.commits) for s in sessions) | |
| total_commits = sum(len(s.commits) for s in sessions) |
| peak_velocity = "late night grinds" | ||
|
|
||
| total_commits = sum(len(s.commits) for s in sessions) | ||
| sum(len(s.commits) for s in sessions) |
There was a problem hiding this comment.
🚨 Bug: NameError: total_commits undefined in wrapped view render
In _render_wrapped_view_ui, the line that computed the commit count was changed from total_commits = sum(len(s.commits) for s in sessions) to a bare expression sum(len(s.commits) for s in sessions), dropping the assignment. Line 1186 still interpolates {total_commits} into the header, so rendering the Wrapped view raises NameError: name 'total_commits' is not defined (the exception is swallowed inside the background worker, silently breaking the whole header). Restore the assignment.
Reassign total_commits so it is defined before use at line 1186.:
total_commits = sum(len(s.commits) for s in sessions)
total_time_str = format_duration(sum(s.duration_seconds for s in sessions))
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| except Exception as e: | ||
| print(f"!!! EXCEPTION IN RENDER TIME SUMMARY: {e} !!!") | ||
| import traceback | ||
| traceback.print_exc() | ||
| raise |
There was a problem hiding this comment.
⚠️ Quality: Leftover debug print/traceback in render_time_summary
The new try/except around render_time_summary calls print(...) and traceback.print_exc() before re-raising. Writing raw text to stdout inside a running Textual TUI corrupts the terminal display, and since the exception is re-raised the wrapper adds no recovery value. Remove the debug print/traceback (or replace with logger.exception(...)), keeping the raise.
Log via the logger instead of printing to stdout, preserving the re-raise.:
except Exception:
logger.exception("Error rendering time summary view")
raise
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
CI failed: Test failures across Python versions caused by an undefined `total_commits` variable in `termstory/tui.py` and an unsupported `ignore_cleanup_errors` parameter in Python 3.9 tests.OverviewAnalyzed 4 CI logs across different Python versions, revealing two distinct change-related test failures: a FailuresUndefined
|
| Auto-apply | Compact |
|
|
Was this helpful? React with 👍 / 👎 | Gitar
| if proj_name == "General / No Project": | ||
| proj_name = "Other" | ||
| project_seconds[proj_name] += s.duration_seconds | ||
| len({s.date_str for s in sessions if s.start_time}) |
There was a problem hiding this comment.
💡 Quality: Dead bare expressions left after removing assignments
Two assignments were reduced to bare expressions whose results are discarded: len({s.date_str for s in sessions if s.start_time}) (was active_days_count) at line 933, and additions - deletions (was net_change) at line 2772. active_days_count and net_change are not referenced later in their respective scopes, so these lines are dead code that compute values and throw them away. Delete them to avoid confusion.
Delete the discarded expression statements.:
# (remove the bare `len({...})` line and the bare `additions - deletions` line entirely)
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
termstory/tui.py (3)
2772-2772: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the dead expression.
additions - deletionsis computed and discarded; the archetype branches below useadditions/deletionsdirectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@termstory/tui.py` at line 2772, Remove the standalone additions - deletions expression from the surrounding logic in tui.py, leaving the existing archetype branches and their direct additions/deletions usage unchanged.
1184-1184: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the
'rpg_class' in locals()guard.rpg_classis unconditionally assigned earlier in both functions (lines 1161 and 1349), so the fallback branch is unreachable.assign_daily_rpg_classalready returns"Level 1 Village Peasant"for empty sessions.♻️ Proposed change
- header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]DAILY CLASS:[/] [dim]{rpg_class if 'rpg_class' in locals() else 'Level 1 Village Peasant'}[/dim]") + header_lines.append(f"[bold cyan]{avatar_lines[9]}[/] [bold cyan]DAILY CLASS:[/] [dim]{rpg_class}[/dim]")Also applies to: 1386-1386
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@termstory/tui.py` at line 1184, Remove the "'rpg_class' in locals()" conditional from the DAILY CLASS formatting in both affected functions, and reference the unconditionally assigned rpg_class directly. Preserve the existing display formatting and the "Level 1 Village Peasant" default provided by assign_daily_rpg_class.
1091-1095: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
logger.exceptioninstead oftraceback.print_exc()in a TUI. Writing to stdout from inside a running Textual app corrupts the screen; the file already has a modulelogger.♻️ Proposed change
- except Exception as e: - print(f"!!! EXCEPTION IN RENDER TIME SUMMARY: {e} !!!") - import traceback - traceback.print_exc() + except Exception: + logger.exception("Failed to render time summary") raise🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@termstory/tui.py` around lines 1091 - 1095, In the render time summary exception handler, replace the stdout print and traceback.print_exc calls with the module logger’s exception-level logging, preserving the existing exception message/context and re-raise behavior. Update the handler associated with the visible “EXCEPTION IN RENDER TIME SUMMARY” message and avoid writing directly to stdout.tests/test_tui.py (4)
20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the avatar stub tolerant of the other call signature.
termstory/tui.pyalso callsget_github_avatar_ascii(guser)with a single argument (inhandle_onboarding_result), which this lambda would reject with aTypeError. Give the parameters defaults matching the real signature.♻️ Proposed change
- monkeypatch.setattr( - "termstory.tui.get_github_avatar_ascii", - lambda operator, width, height, on_resolved: [""] * height - ) + monkeypatch.setattr( + "termstory.tui.get_github_avatar_ascii", + lambda username, width=12, height=7, on_resolved=None: [""] * height, + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_tui.py` around lines 20 - 25, Update the mock_github_avatar_fetch fixture’s lambda stub for get_github_avatar_ascii to accept omitted width, height, and on_resolved arguments by supplying defaults matching the real function signature, while preserving its existing height-based placeholder result.
584-584: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace the trailing
asyncio.sleep(0.5)withawait pilot.pause(). An unconditional half-second wait just to let state settle slows the suite and hides what is actually being awaited.As per coding guidelines, "Avoid sleeps unless the Textual pilot needs a message-loop tick; prefer
await pilot.pause()for UI synchronization."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_tui.py` at line 584, In the test flow containing the trailing asyncio.sleep(0.5), replace the unconditional delay with await pilot.pause() so synchronization uses the Textual pilot’s message-loop tick without adding a fixed wait.Source: Coding guidelines
1055-1055: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOverriding the private
_show_node_detailscouples the test to internals.app.auto_select_today_on_mount = False(line 1054) already suppresses the initial selection; if this stub is needed only to block tree-selection re-renders, prefer not selecting nodes rather than replacing the method.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_tui.py` at line 1055, Remove the test’s override of the private app._show_node_details method. Rely on app.auto_select_today_on_mount = False and adjust the test setup or interactions to avoid selecting nodes, preserving the intended behavior without coupling the test to the app’s internal method.
634-643: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated widget-polling block into a helper. The same "poll up to 50× for
#id, then press" pattern (with an inlineimport asyncioand bareexcept Exception) is duplicated at lines 534-542, 568-575, 704-712, 772-780, 847-855 and 1132-1140. A single module-level async helper usingawait pilot.pause()andNoMatcheswould be tighter and match the guideline preference for pilot synchronization over sleeps.♻️ Sketch
from textual.css.query import NoMatches async def wait_for(app, pilot, selector, attempts=50): for _ in range(attempts): try: return app.query_one(selector) except NoMatches: await pilot.pause() raise AssertionError(f"{selector} never appeared")As per coding guidelines, "Avoid sleeps unless the Textual pilot needs a message-loop tick; prefer
await pilot.pause()for UI synchronization."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_tui.py` around lines 634 - 643, Extract the duplicated widget-polling loops in the test module into one module-level async helper, such as wait_for, accepting app, pilot, selector, and attempts. Use NoMatches instead of bare Exception, await pilot.pause() between attempts, and raise a clear AssertionError if the selector never appears; update each affected test to call the helper and preserve the existing button-press behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@termstory/tui.py`:
- Line 1171: Assign the commit-count expression in `_render_wrapped_view_ui` to
the `total_commits` variable before it is used in the formatting at line 1186,
preserving the existing sum across `sessions`.
---
Nitpick comments:
In `@termstory/tui.py`:
- Line 2772: Remove the standalone additions - deletions expression from the
surrounding logic in tui.py, leaving the existing archetype branches and their
direct additions/deletions usage unchanged.
- Line 1184: Remove the "'rpg_class' in locals()" conditional from the DAILY
CLASS formatting in both affected functions, and reference the unconditionally
assigned rpg_class directly. Preserve the existing display formatting and the
"Level 1 Village Peasant" default provided by assign_daily_rpg_class.
- Around line 1091-1095: In the render time summary exception handler, replace
the stdout print and traceback.print_exc calls with the module logger’s
exception-level logging, preserving the existing exception message/context and
re-raise behavior. Update the handler associated with the visible “EXCEPTION IN
RENDER TIME SUMMARY” message and avoid writing directly to stdout.
In `@tests/test_tui.py`:
- Around line 20-25: Update the mock_github_avatar_fetch fixture’s lambda stub
for get_github_avatar_ascii to accept omitted width, height, and on_resolved
arguments by supplying defaults matching the real function signature, while
preserving its existing height-based placeholder result.
- Line 584: In the test flow containing the trailing asyncio.sleep(0.5), replace
the unconditional delay with await pilot.pause() so synchronization uses the
Textual pilot’s message-loop tick without adding a fixed wait.
- Line 1055: Remove the test’s override of the private app._show_node_details
method. Rely on app.auto_select_today_on_mount = False and adjust the test setup
or interactions to avoid selecting nodes, preserving the intended behavior
without coupling the test to the app’s internal method.
- Around line 634-643: Extract the duplicated widget-polling loops in the test
module into one module-level async helper, such as wait_for, accepting app,
pilot, selector, and attempts. Use NoMatches instead of bare Exception, await
pilot.pause() between attempts, and raise a clear AssertionError if the selector
never appears; update each affected test to call the helper and preserve the
existing button-press behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 361e2f65-5709-475e-a9c9-01f7187a8c18
📒 Files selected for processing (2)
termstory/tui.pytests/test_tui.py
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
- GitHub Check: Greptile Review
- GitHub Check: Test (Python 3.10)
- GitHub Check: Test (Python 3.11)
- GitHub Check: Test (Python 3.12)
- GitHub Check: Test (Python 3.9)
- GitHub Check: Gitar
🧰 Additional context used
📓 Path-based instructions (3)
termstory/tui.py
📄 CodeRabbit inference engine (termstory/AGENTS.md)
termstory/tui.py: Use Textual'scall_after_refresh(...)when UI updates must occur after a screen refresh.
Use Textual's@workdecorator for appropriate background tasks.
Files:
termstory/tui.py
termstory/*.py
📄 CodeRabbit inference engine (termstory/AGENTS.md)
Follow the existing project structure and naming conventions when adding features.
Files:
termstory/tui.py
tests/**/*.py
📄 CodeRabbit inference engine (tests/AGENTS.md)
tests/**/*.py: Use@pytest.mark.asynciofor asynchronous Textual tests.
Useasync with app.run_test() as pilot:for Textual application tests.
Callawait pilot.pause()after actions that require Textual's message loop to process callbacks or screen updates.
For modal dismiss tests affected by Textual 8.x, installinstall_sync_dismiss_workaround(monkeypatch)before enteringrun_test().
Prefertempfile.TemporaryDirectory()ortmp_pathfor filesystem isolation in tests.
UseDatabase(":memory:")when a test only needs an in-memory SQLite database.
Mock AI providers instead of making network calls; do not require real API keys, external services, shell history, or a user's local terminal state.
Keep mocked provider responses small and deterministic.
CreateDatabaseinstances in temporary locations and callinit_db()before saving data.
Assert on persisted rows or returned models rather than private implementation details, except when testing migrations or cache behavior.
CLI tests should use pytest helpers such astmp_path,monkeypatch, and existing Typer test utilities.
Keep tests direct and readable, favoring explicit setup over shared fixtures when setup is small.
Match the existing import style in the neighboring test file.
Avoid sleeps unless the Textual pilot needs a message-loop tick; preferawait pilot.pause()for UI synchronization.
Files:
tests/test_tui.py
🔇 Additional comments (6)
termstory/tui.py (4)
933-933: Dead statement still present.len({s.date_str ...})result is discarded.
54-54: LGTM!
912-913: LGTM!Also applies to: 945-945
1212-1212: LGTM!Also applies to: 1224-1226, 1808-1808, 2756-2756
tests/test_tui.py (2)
112-112: LGTM!Also applies to: 223-223, 319-319, 341-341
527-527: LGTM!Also applies to: 764-764, 832-832, 1414-1414
| peak_velocity = "late night grinds" | ||
|
|
||
| total_commits = sum(len(s.commits) for s in sessions) | ||
| sum(len(s.commits) for s in sessions) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Critical: total_commits is never assigned — _render_wrapped_view_ui will raise NameError. Line 1171 computes the commit count but discards it, while line 1186 formats {total_commits}. Every wrapped view render (month / overall / timeline node) fails.
🐛 Proposed fix
- sum(len(s.commits) for s in sessions)
+ total_commits = sum(len(s.commits) for s in sessions)Also applies to: 1186-1186
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@termstory/tui.py` at line 1171, Assign the commit-count expression in
`_render_wrapped_view_ui` to the `total_commits` variable before it is used in
the formatting at line 1186, preserving the existing sum across `sessions`.
|
/ecsoc |
|
miku 🏷️ Added |
|
Thanks @Diwakar-odds — the necromancer metric is a nice companion to the vampire index. Leaving this open rather than merging, because it needs a rework pass:
If you rebase and slim it down to just the necromancer computation + one header line (no try-wrap, no debug import), it should merge cleanly. |
Summary
Introduces the Necromancer Score metric for user behavior analysis.
Motivation
Closes #45.
Changes
termstory/insights.py:calculate_necromancer_scoreto tally meaningful sessions resurrecting 180+ day dormant projects.termstory/tui.py:Acceptance Criteria
Impact & Side Effects
No breaking changes. Adds visual metrics to the TUI.
How to Test
termstory guiand open any dashboard timeframe view to verify the headers.NECROMANCERmetric correctly.Quality Checklist