Skip to content

Tracing dashboard redesign + robust project-memory capacity handling - #126

Closed
elkaix wants to merge 8 commits into
mainfrom
feat/vis-dashboard-redesign
Closed

Tracing dashboard redesign + robust project-memory capacity handling#126
elkaix wants to merge 8 commits into
mainfrom
feat/vis-dashboard-redesign

Conversation

@elkaix

@elkaix elkaix commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

This branch bundles the tracing-dashboard redesign with a robustness fix for the project-memory tool.

Project-memory capacity handling (this session)

A user hit a loop where the agent thrashed through 5 failed memory writes against a near-full store and had to be interrupted. Root cause: the budget check silently added a 3-char entry delimiter the error never disclosed, so 2085/2200, entry (113) exceeds read as satisfiable (2085+113 < 2200) but wasn't — and the agent had no way to see the true ceiling or what was stored.

  • Delimiter-aware, legible errors: rejections now report exact free chars, the entry's real cost (content + separator), used/limit, and a compact inventory (index, size, preview) so the next remove/replace is guided, not guessed.
  • New read-only list action for mid-session introspection (status()/capacity() on the store).
  • Full-store education: on a capacity rejection the user-facing tool card explains what happened (nothing lost, task continues) and how to free space.
  • Raised limits: MEMORY 2200→5000, USER 1375→2500 (within the 8 KB injection budget).
  • /memory now shows per-store capacity and a "nearly full" panel at ≥85%.
  • Deliberately did not add SQLite/manifest/WAL — recall.py + consolidation.py + JOURNAL.md + atomic writes already cover retrieval/compaction/durability.

Tracing dashboard redesign

  • New Usage page (heatmap + trend chart), richer statistics view, blue-accent polish, metric/card components.
  • Slash completer ranks command-name matches above alias matches.
  • TUI: diff context lines render in normal text color.
  • MCP skill bridge, agent guardrails, session UX fixes; react bumped to 19.2.7 in vis/web.

Verification

  • New tests pin delimiter-overhead rejection, exact free-space reporting, status/list, replace-overage, and the /memory capacity line.
  • Memory/recall/consolidation/tools/slash suites pass; ruff + format + pyright clean on touched files.

Notes / tradeoffs

  • With raised limits, heavily multibyte memory (e.g. Arabic) can exceed the 8 KB injection budget sooner; snapshot() already truncates gracefully and the header points the agent to the files. Left INJECTION_BUDGET_BYTES at 8 KB to avoid inflating every prompt.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added designer-skill MCP bridge support for design system integration
    • Introduced best_practices_always config option to auto-inject best-practices guidance
    • Enhanced /recap command with on/off toggle plus inline autosuggest
    • Added /memory list action with capacity and inventory reporting
    • New usage analytics dashboard with heatmap and trend charts
  • Improvements

    • Strengthened agent guardrails with enhanced verification requirements
    • Improved batch file-edit error messages guiding users toward single-edit solutions
    • Better memory capacity warnings when approaching limits
    • Enhanced edit-diff previews with improved context line readability
    • UI polish: login provider styling, theme refinements, reduced-motion support
  • Reliability

    • Session scratch files now cleaned up on exception-based exits

elkaix added 8 commits June 13, 2026 08:44
ReadSkill now resolves a connected MCP server name (including plugin-style
aliases like designer-skill:designer-skill) to a bridge listing that server's
tools instead of "Skill not found", and ships a designer-skill stub that routes
frontend work to the MCP tools.

best_practices_always config option folds the full /best-practices engineering
guidance into the root session's system prompt at startup (default off), so the
guardrails apply to every new session without running the command.

StrReplaceFile returns a precise, actionable error when a multi-edit batch fails
schema validation (e.g. entries collapsed by a streaming glitch), naming the bad
entries and steering toward single-edit calls; valid edits are never partially
applied.

The default system prompt now requires absence claims ("no em-dashes", "no
leftover debug", "matches the source") to be backed by an actual zero-hit scan,
and to re-ask rather than act on a self-authored reading of a non-responsive
clarifying answer.

Also: /recap on|off toggle with grey autosuggest; recaps strip <system-reminder>
blocks; configured /login providers get distinct state styling; braille
background spinner and hanging-indent working tips; scratch files are cleaned up
on exception-path session exit. Removes leftover debug instrumentation from the
skill tool.
Unchanged context lines in file-edit diff snippets (Write, StrReplaceFile, and
all diff cards) used a muted grey (tool_diff_context), which read as dimmed
against the normal body text. Match the normal `text` token instead — terminal
default foreground in dark, #213853 in light — so edited-file previews are easy
to read. Added/removed lines keep their green/red styling, so changes still
stand out.
Refresh the Statistics and Sessions surfaces of the agent tracing
visualizer and add a new Usage page, keeping the existing neutral/zinc
identity (no new brand accent) and the dependency-free SVG charts.

- Add shared shadcn-style Card primitives (12px radius, subtle shadow)
- Statistics: icon-tile metric cards with helper lines, titled chart/
  tool/table cards, rounded bars, bordered hover-row project table,
  width-contained layout, and an empty state
- Sessions: focus rings and a search hint in the toolbar, softer card
  radius with hover lift, folder-tile project group headers
- New Usage page: summary cards, a GitHub-style activity heatmap keyed
  on daily turns (monochrome intensity ramp), and a turn-trend chart
- Header: app icon tile, subtitle, accessible theme toggle; add the
  Usage tab as polished pills
- Add a prefers-reduced-motion safety block to global CSS

Behavior, data flow, and DOM/event contracts are unchanged.
Typing /report surfaced /report_error first because its "report" alias
was an exact match, ranked above /reports (a command-name prefix match).
Rank by match tier (name exact, name prefix, alias exact, alias prefix)
then by command-name length, so the closest command name wins. /report
now lists /reports first.

Drop the now-unused _command_lookup and add regression tests.
Apply a soft-enterprise analytics treatment across the tracing
visualizer and make the Usage visualizations feel intentional.

- Introduce a single restrained blue accent (primary/ring tokens) for
  light and dark; charts and the heatmap now carry visual hierarchy
- Shared premium MetricCard (rounded-2xl, icon tile, hover lift); used
  by Statistics and Usage
- Usage: heatmap and a new Usage Insights panel sit side by side to use
  the available width; larger blue GitHub-style heatmap with a
  Turns/Sessions toggle and Less/More legend
- New area trend chart with gradient fill, gridlines, axis labels, and a
  hover tooltip (dependency-free SVG)
- Statistics: single accent tool bars with a neutral error badge instead
  of red segments; blue daily-usage series
- Soft muted page background and consistent rounded-2xl cards

Charts remain hand-rolled SVG (no recharts). Behavior and data flow are
unchanged.
Align the web and vis frontends on the latest stable React (19.2.7,
@types/react 19.2.17) on top of vite 8, and refresh both lockfiles.
Both frontends type-check and build cleanly under vite 8 + react 19.2.7.
The chart used a fixed 600x140 viewBox with maxHeight, so
preserveAspectRatio letterboxed it: the plot rendered ~600px wide and
floated centered in a much wider card. Measure the container width with
a ResizeObserver and render the SVG full width (taller, with gridlines
and a sessions Y axis), matching the Usage trend chart.
…cation

The project-memory budget check silently added a 3-char entry delimiter that
the rejection message never disclosed, so a near-full store reported e.g.
"2085/2200, entry (113) exceeds" — math that reads as satisfiable (2085+113<2200)
but isn't. With no visibility into the true ceiling or what was stored, the agent
could only blind-shrink the entry and loop until interrupted.

- project_memory: delimiter-aware accounting; rejections now report exact free
  chars, the entry's real cost (content + separator), used/limit, and a compact
  inventory (index, size, preview) so the next remove/replace is guided. Add
  status() and capacity(); flag capacity failures via MemoryOpResult.full.
- Memory tool: new read-only `list` action for mid-session introspection; on a
  full-store rejection, append a plain-language explanation (nothing lost, task
  continues, how to free space) to the user-facing tool card.
- Raise limits MEMORY 2200->5000, USER 1375->2500 (within the 8 KB injection budget).
- /memory: show per-store capacity and a "nearly full" guidance panel at >=85%.
- memory.md: best-effort housekeeping guidance — don't loop on rejection.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces best-practices auto-injection via config flag, expands memory capacity reporting with status/inventory, adds MCP-skill fallback resolution, improves batch validation errors, implements /recap on|off toggle with persistence, sanitizes recaps by stripping internal blocks, refactors statistics/usage frontend, and upgrades React dependencies.

Changes

Configuration, Memory Capacity & Recap Toggle

Layer / File(s) Summary
Always-on Best-Practices Config & Wiring
src/pythinker_code/config.py, src/pythinker_code/app.py, src/pythinker_code/prompts/__init__.py, tests/core/test_best_practices_slash.py, tests/core/test_config.py
Config.best_practices_always (default false) gates conditional post-processing: apply_always_on_best_practices() appends guidance to session prompt at startup, stripping the manual lead-in from BEST_PRACTICES content.
Project Memory Capacity Reporting & Status
src/pythinker_code/project_memory.py, tests/core/test_project_memory.py
Limits increased (5000/2500 chars), MemoryOpResult.full flag added, add/replace over-limit checks now use used + overhead accounting, returning detailed inventory and free-space figures; new capacity() and status() report current usage/limit/free with preview.
Memory Tool List Action & Full Errors
src/pythinker_code/tools/memory/__init__.py, src/pythinker_code/tools/memory/memory.md, tests/tools/test_memory_tool.py
Params.action expanded to "list", tool calls status() on list requests; on-full errors append guidance explaining write rejection and free-space options.
/memory Command Capacity Display
src/pythinker_code/ui/shell/slash.py
/memory output now shows capacity (used/limit/free) for both project and user stores; warns and advises when ≥85% full.
/recap on|off Toggle with Config Persistence
src/pythinker_code/soul/slash.py, tests/core/test_slash_recap.py
New `/recap on
Settings Recap Alias & Parsing
src/pythinker_code/ui/shell/slash.py, tests/ui_and_conv/test_settings_recaps_slash.py
/settings recap[s] on|off accepts both singular and plural forms; test verifies singular alias recap on behavior.

MCP Skill Bridge & ReadSkill Fallback

Layer / File(s) Summary
MCP Bridge Utility Functions
src/pythinker_code/tools/skill/_mcp_bridge.py, tests/tools/test_mcp_skill_bridge.py
New helpers: skill_lookup_keys() generates candidate normalized keys (including prefix:suffix), find_mcp_server_for_skill_name() matches candidates to MCP server, mcp_skill_bridge_content() generates markdown listing tools and usage.
ReadSkill MCP Fallback & Error Hints
src/pythinker_code/tools/skill/__init__.py, tests/tools/test_skill_tool.py
ReadSkillTool uses candidate lookup keys; when no local skill matches, attempts MCP bridge resolution; on failure, includes connected MCP servers in error hint. Plugin-style alias resolution (e.g., designer-skill:designer-skill) now supported.
Designer-Skill MCP Documentation Stub
src/pythinker_code/skills/designer-skill/SKILL.md, tests/core/test_builtin_authoring_skills.py
Prescriptive skill stub defining required MCP tools (get_design_system, get_reference, dispatch_intent, apply_designer, anti_slop_checklist), session preflight workflow, aliases, and disconnected-server guidance; added to builtin skills parametrization.

StrReplaceFile Batch Validation & UI Enhancements

Layer / File(s) Summary
StrReplaceFile Batch Validation & Error Reporting
src/pythinker_code/tools/file/replace.py, tests/tools/test_str_replace_file.py
New _malformed_edit_batch_error() detects and reports specifically-malformed edit lists (naming bad indices), steering users to single-edit calls; call() override validates upfront, returning focused error or delegating to __call__.
Exact /recap Suggestions & Slash Completer Ranking
src/pythinker_code/ui/shell/prompt.py, tests/ui_and_conv/test_prompt_tips.py, tests/ui_and_conv/test_slash_completer.py
SlashCommandAutoSuggest accepts exact_suggestions callback; CustomPromptSession wires /recap suggestions toggling " on"/" off" based on turn_recaps_provider; SlashCommandCompleter replaces precomputed lookup with tiered on-the-fly matcher (exact name > prefix > exact alias > alias prefix).
Background Spinner Timing & Agent-Status Rendering
src/pythinker_code/ui/shell/prompt.py, src/pythinker_code/ui/shell/visualize/_live_view.py, tests/ui_and_conv/test_live_view_notifications.py, tests/ui_and_conv/test_visualize_running_prompt.py
Background-working-status frame uses elapsed-time-based active_marker_frame() instead of blink; inserted into agent output when no pinned tail; tips wrapped via render_message_response with hanging indent.
Selector Item Separate Icon/Text Styling
src/pythinker_code/ui/shell/selector.py, src/pythinker_code/ui/shell/selectors/oauth.py, tests/ui_and_conv/test_selectors_simple.py
SelectorItem gains description_icon_style and description_text_style fields; _format_item_line renders icon (first char) and text (rest) with separate styles; OAuth selector applies per-status styling helpers.
Prompt & Diff Context Theme Colors
src/pythinker_code/ui/theme.py
Adds success/warning completion-menu metadata colors for both dark and light themes; updates tool_diff_context to terminal default (dark) or theme text (light) for readable unchanged context lines.

Session Scratch Cleanup & Recap Sanitization

Layer / File(s) Summary
Scratch File Cleanup on Exception & Test Refactor
src/pythinker_code/cli/__init__.py, tests/core/test_session.py
CLI reload-loop exception handler now calls best-effort cleanup_session_scratch() for latest session; test helper _simulate_exception_cleanup() mirrors production cleanup (suppresses errors, conditionally deletes empty sessions).
System-Reminder Block Stripping from Recaps
src/pythinker_code/session_recap.py, tests/test_session_recap.py
New regex removes <system-reminder>...</system-reminder> blocks from recap source before normalization, preventing harness context leakage.

System Prompt Guardrails & Changelog

Layer / File(s) Summary
Enhanced Evidence & Clarification Rules
src/pythinker_code/agents/default/system.md
"Evidence before claims" now defines verification canonically and tightens absence-claim conditions; "Ask vs. act" requires re-asking when clarification doesn't resolve ambiguity.
Changelog Updates
CHANGELOG.md
Documents MCP bridge, always-on best-practices, batch validation, guardrails, recap toggle, sanitization, UI polish, and scratch cleanup features.

Frontend: Usage Dashboard & Statistics Refactor

Layer / File(s) Summary
Usage Heatmap & Trend Chart Components
vis/src/features/usage/usage-heatmap.tsx, vis/src/features/usage/usage-trend-chart.tsx
Calendar-style heatmap with turns/sessions metric toggle, heat-level thresholding, and week-by-week grid; SVG trend chart with responsive width, Y scaling, hover interaction, vertical guide + tooltip.
Usage View Full Dashboard
vis/src/features/usage/usage-view.tsx
Fetches aggregate stats, manages loading/error/empty states, derives totals/peak-day/active-count metrics; renders heatmap (with metric toggle + legend), insights card (peak/average), and trend chart.
Statistics View Refactor with Metric Cards & SVG Charts
vis/src/features/statistics/statistics-view.tsx
Replaces previous components: DailyUsageChart as SVG line/area, ToolUsageChart as card-based bars with error badge, ProjectTable with Card wrapper and truncated path display; layout uses four MetricCard summary metrics, heatmap, and two-column tool/project section.
Reusable Card UI Components
vis/src/components/ui/card.tsx, vis/src/components/metric-card.tsx
Exports Card, CardHeader, CardTitle, CardDescription, CardContent div wrappers with Tailwind styles and MetricCard for labeled metrics with icons.
App.tsx Usage View Integration
vis/src/App.tsx
Adds Usage explorer mode, renders Usage tab with LineChart icon, updates session header with Activity badge and backdrop-blur, wires explorer rendering.
Explorer Styling & Session Card Polish
vis/src/features/sessions-explorer/...
Updates toolbar buttons with aria-pressed, focus-visible rings, consistent sizing; project group toggle adds aria-expanded and badge layout; session card refactored with group, rounded-xl, stronger hover/shadow; scroll container adds muted background.
Theme Color & Motion Preference Updates
vis/src/index.css
Primary/ring colors updated to oklch() values; adds prefers-reduced-motion: reduce media query disabling animations.

Dependencies & Build Configuration

Layer / File(s) Summary
React & Types Dependency Upgrades
vis/package.json, web/package.json
react/react-dom bumped from ^19.2.0 to ^19.2.7; @types/react bumped from ^19.2.5 to ^19.2.17.
PyInstaller Data & Hidden Imports
tests/utils/test_pyinstaller_utils.py
Adds designer-skill SKILL.md to bundled data; adds pythinker_code.tools.skill._mcp_bridge to hidden imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, bug, dependencies

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vis-dashboard-redesign

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pythinker_code/app.py (1)

355-377: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Best-practices modification is lost on resumed sessions.

When a resumed session has a saved context.system_prompt (line 368), the agent replacement at line 375 uses refresh_system_prompt_scratchpad_section(context.system_prompt, ...), which operates on the old context prompt—discarding the best-practices enhancement applied at line 360.

Result: best_practices_always only works for new sessions, not resumed ones.

🔧 Proposed fix: apply best practices after context restoration

Move the best-practices application after the context restoration block so it applies to both new and resumed sessions:

         agent = await load_agent(
             agent_file,
             runtime,
             mcp_configs=mcp_configs or [],
             start_mcp_loading=not defer_mcp_loading,
         )
-
-        if runtime.config.best_practices_always:
-            from pythinker_code.prompts import apply_always_on_best_practices
-
-            agent = dataclasses.replace(
-                agent,
-                system_prompt=apply_always_on_best_practices(agent.system_prompt, enabled=True),
-            )
 
         if startup_progress is not None:
             startup_progress("Restoring conversation...")
         context = Context(session.context_file)
         await context.restore()
 
         if context.system_prompt is not None:
             from pythinker_code.scratchpad import refresh_system_prompt_scratchpad_section
 
             refreshed_system_prompt = refresh_system_prompt_scratchpad_section(
                 context.system_prompt,
                 runtime.builtin_args.PYTHINKER_SCRATCHPAD_SECTION,
             )
             agent = dataclasses.replace(agent, system_prompt=refreshed_system_prompt)
         else:
             await context.write_system_prompt(agent.system_prompt)
+
+        if runtime.config.best_practices_always:
+            from pythinker_code.prompts import apply_always_on_best_practices
+
+            agent = dataclasses.replace(
+                agent,
+                system_prompt=apply_always_on_best_practices(agent.system_prompt, enabled=True),
+            )
+            # Update context so the enhanced prompt is persisted
+            await context.write_system_prompt(agent.system_prompt)
 
         soul = PythinkerSoul(agent, context=context)

This ensures the best-practices text is applied to the final system prompt (whether from agent spec or restored context) and persisted for future resumes.

🤖 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 `@src/pythinker_code/app.py` around lines 355 - 377, The best-practices
augmentation (runtime.config.best_practices_always +
apply_always_on_best_practices and dataclasses.replace of agent.system_prompt)
must be applied after restoring/refreshing the Context so resumed sessions keep
the enhancement; move the runtime.config.best_practices_always block to follow
the Context(session.context_file); await context.restore(); logic and after
refresh_system_prompt_scratchpad_section/context.write_system_prompt so you call
apply_always_on_best_practices on the final system prompt (whether refreshed
from context or the agent spec) and then dataclasses.replace the agent with that
resulting system_prompt (and persist it via context.write_system_prompt if
needed).
🤖 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 `@src/pythinker_code/cli/__init__.py`:
- Around line 1206-1213: Replace the silent contextlib.suppress around the
cleanup call so failures are still best-effort but logged: instead of "with
contextlib.suppress(Exception): await cleanup_session_scratch(...)" wrap the
await in a try/except Exception as e and log the exception (e.g., via
logging.getLogger(__name__).exception(...) or the module's existing logger)
including context (_latest_created_session.id, .work_dir, .title) so
cleanup_session_scratch failures are recorded for diagnosis.

In `@src/pythinker_code/tools/skill/_mcp_bridge.py`:
- Around line 37-67: The candidate-extraction logic in
find_mcp_server_for_skill_name duplicates skill_lookup_keys; replace lines that
build candidates (the raw/suffix/prefix block) with a call to
skill_lookup_keys(skill_name) to obtain the candidate list, then iterate that
list (normalizing with normalize_skill_name as before) while using
_index_mcp_servers(mcp_tools) to find a matching server; keep the early return
of server, tools and the seen dedup set logic if needed, and leave
_index_mcp_servers and normalize_skill_name calls intact.

In `@tests/core/test_session.py`:
- Around line 686-689: The ARG001 lint warning is triggered because the test
fixture parameter isolated_share_dir is unused; rename the parameter to
_isolated_share_dir in the test_exception_cleanup_removes_session_scratch_file
test to acknowledge the fixture side effects without using it, and apply the
same rename to the other new test that also declares isolated_share_dir so both
tests compile lint-clean (update the function signatures only, e.g., def
test_exception_cleanup_removes_session_scratch_file(... isolated_share_dir:
Path, ...) -> ... to ... _isolated_share_dir: Path ...).
- Around line 653-669: The helper _simulate_exception_cleanup currently calls
await session.delete() unprotected, diverging from the CLI which suppresses
deletion failures; modify _simulate_exception_cleanup so that the await
session.delete() call is wrapped in contextlib.suppress(Exception) (reuse the
existing contextlib import) so both cleanup_session_scratch and the
session.delete() step suppress exceptions and mirror the CLI behavior; keep
checks using session.is_empty() and ensure no other behavior changes.

In `@tests/ui_and_conv/test_settings_recaps_slash.py`:
- Around line 62-80: The test mutates config_for_save but doesn't assert that
the persistence path was invoked; update
test_recap_singular_on_persists_and_reloads to assert the save call on the
mocked save_config the same way the plural-form test does: verify
shell_slash.save_config was called (e.g., assert called_once) and that it was
invoked with the expected arguments (the config file
path/runtime.config.source_file and config_for_save) so the test ensures
persistence, not just in-memory mutation.

In `@tests/ui_and_conv/test_visualize_running_prompt.py`:
- Around line 396-399: Add explicit return type annotations (-> str) to the test
stub methods in class _BlockingTaskOutputDelegate: change the signatures of
render_agent_status(self, columns: int) and render_pinned_status_tail(self,
columns: int) to include -> str so they read render_agent_status(self, columns:
int) -> str and render_pinned_status_tail(self, columns: int) -> str.

---

Outside diff comments:
In `@src/pythinker_code/app.py`:
- Around line 355-377: The best-practices augmentation
(runtime.config.best_practices_always + apply_always_on_best_practices and
dataclasses.replace of agent.system_prompt) must be applied after
restoring/refreshing the Context so resumed sessions keep the enhancement; move
the runtime.config.best_practices_always block to follow the
Context(session.context_file); await context.restore(); logic and after
refresh_system_prompt_scratchpad_section/context.write_system_prompt so you call
apply_always_on_best_practices on the final system prompt (whether refreshed
from context or the agent spec) and then dataclasses.replace the agent with that
resulting system_prompt (and persist it via context.write_system_prompt if
needed).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 72b2bf36-a29e-4e52-823c-e2f3b813f276

📥 Commits

Reviewing files that changed from the base of the PR and between d020e4e and b69177e.

⛔ Files ignored due to path filters (2)
  • vis/package-lock.json is excluded by !**/package-lock.json
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (56)
  • CHANGELOG.md
  • src/pythinker_code/agents/default/system.md
  • src/pythinker_code/app.py
  • src/pythinker_code/cli/__init__.py
  • src/pythinker_code/config.py
  • src/pythinker_code/project_memory.py
  • src/pythinker_code/prompts/__init__.py
  • src/pythinker_code/session_recap.py
  • src/pythinker_code/skills/designer-skill/SKILL.md
  • src/pythinker_code/soul/slash.py
  • src/pythinker_code/tools/file/replace.py
  • src/pythinker_code/tools/memory/__init__.py
  • src/pythinker_code/tools/memory/memory.md
  • src/pythinker_code/tools/skill/__init__.py
  • src/pythinker_code/tools/skill/_mcp_bridge.py
  • src/pythinker_code/tools/skill/description.md
  • src/pythinker_code/ui/shell/__init__.py
  • src/pythinker_code/ui/shell/prompt.py
  • src/pythinker_code/ui/shell/selector.py
  • src/pythinker_code/ui/shell/selectors/oauth.py
  • src/pythinker_code/ui/shell/slash.py
  • src/pythinker_code/ui/shell/visualize/_live_view.py
  • src/pythinker_code/ui/theme.py
  • tests/core/test_best_practices_slash.py
  • tests/core/test_builtin_authoring_skills.py
  • tests/core/test_config.py
  • tests/core/test_project_memory.py
  • tests/core/test_session.py
  • tests/core/test_slash_recap.py
  • tests/test_session_recap.py
  • tests/tools/test_mcp_skill_bridge.py
  • tests/tools/test_memory_tool.py
  • tests/tools/test_skill_tool.py
  • tests/tools/test_str_replace_file.py
  • tests/ui_and_conv/test_live_view_notifications.py
  • tests/ui_and_conv/test_memory_slash.py
  • tests/ui_and_conv/test_prompt_tips.py
  • tests/ui_and_conv/test_selectors_simple.py
  • tests/ui_and_conv/test_settings_recaps_slash.py
  • tests/ui_and_conv/test_slash_completer.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
  • tests/utils/test_pyinstaller_utils.py
  • vis/package.json
  • vis/src/App.tsx
  • vis/src/components/metric-card.tsx
  • vis/src/components/ui/card.tsx
  • vis/src/features/sessions-explorer/explorer-toolbar.tsx
  • vis/src/features/sessions-explorer/project-group.tsx
  • vis/src/features/sessions-explorer/session-card.tsx
  • vis/src/features/sessions-explorer/sessions-explorer.tsx
  • vis/src/features/statistics/statistics-view.tsx
  • vis/src/features/usage/usage-heatmap.tsx
  • vis/src/features/usage/usage-trend-chart.tsx
  • vis/src/features/usage/usage-view.tsx
  • vis/src/index.css
  • web/package.json

Comment on lines +1206 to +1213
with contextlib.suppress(Exception):
from pythinker_code.scratchpad import cleanup_session_scratch

await cleanup_session_scratch(
_latest_created_session.work_dir,
session_id=_latest_created_session.id,
session_title=_latest_created_session.title,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log best-effort cleanup failures instead of fully suppressing them.

At Line 1206, exceptions from cleanup_session_scratch(...) are swallowed without any trace. Keep it best-effort, but log failures so repeated scratch-cleanup regressions are diagnosable.

Proposed fix
-                with contextlib.suppress(Exception):
-                    from pythinker_code.scratchpad import cleanup_session_scratch
-
-                    await cleanup_session_scratch(
-                        _latest_created_session.work_dir,
-                        session_id=_latest_created_session.id,
-                        session_title=_latest_created_session.title,
-                    )
+                try:
+                    from pythinker_code.scratchpad import cleanup_session_scratch
+
+                    await cleanup_session_scratch(
+                        _latest_created_session.work_dir,
+                        session_id=_latest_created_session.id,
+                        session_title=_latest_created_session.title,
+                    )
+                except Exception:
+                    logger.debug(
+                        "Best-effort scratch cleanup failed for session {session_id}",
+                        session_id=_latest_created_session.id,
+                        exc_info=True,
+                    )

As per coding guidelines, exception handlers that silently swallow errors should be logged or re-raised.

🤖 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 `@src/pythinker_code/cli/__init__.py` around lines 1206 - 1213, Replace the
silent contextlib.suppress around the cleanup call so failures are still
best-effort but logged: instead of "with contextlib.suppress(Exception): await
cleanup_session_scratch(...)" wrap the await in a try/except Exception as e and
log the exception (e.g., via logging.getLogger(__name__).exception(...) or the
module's existing logger) including context (_latest_created_session.id,
.work_dir, .title) so cleanup_session_scratch failures are recorded for
diagnosis.

Source: Coding guidelines

Comment on lines +37 to +67
def find_mcp_server_for_skill_name(
skill_name: str,
mcp_tools: Mapping[str, object],
) -> tuple[str, list[str]] | None:
"""Match a skill name (or plugin alias) to a connected MCP server."""
servers = _index_mcp_servers(mcp_tools)
if not servers:
return None

candidates: list[str] = []
raw = skill_name.strip()
if raw:
candidates.append(raw)
if ":" in raw:
suffix = raw.rsplit(":", 1)[-1].strip()
if suffix:
candidates.append(suffix)
prefix = raw.split(":", 1)[0].strip()
if prefix:
candidates.append(prefix)

seen: set[str] = set()
for candidate in candidates:
norm = normalize_skill_name(candidate)
if norm in seen:
continue
seen.add(norm)
for server, tools in servers.items():
if normalize_skill_name(server) == norm:
return server, tools
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Eliminate duplicated candidate-building logic.

Lines 46-56 duplicate the candidate extraction logic from skill_lookup_keys (lines 10-20). Call skill_lookup_keys(skill_name) to get normalized candidates, then iterate through them directly.

♻️ Proposed refactor
 def find_mcp_server_for_skill_name(
     skill_name: str,
     mcp_tools: Mapping[str, object],
 ) -> tuple[str, list[str]] | None:
     """Match a skill name (or plugin alias) to a connected MCP server."""
     servers = _index_mcp_servers(mcp_tools)
     if not servers:
         return None
 
-    candidates: list[str] = []
-    raw = skill_name.strip()
-    if raw:
-        candidates.append(raw)
-    if ":" in raw:
-        suffix = raw.rsplit(":", 1)[-1].strip()
-        if suffix:
-            candidates.append(suffix)
-        prefix = raw.split(":", 1)[0].strip()
-        if prefix:
-            candidates.append(prefix)
-
-    seen: set[str] = set()
-    for candidate in candidates:
-        norm = normalize_skill_name(candidate)
-        if norm in seen:
-            continue
-        seen.add(norm)
+    lookup_keys = skill_lookup_keys(skill_name)
+    seen: set[str] = set()
+    for norm in lookup_keys:
+        if norm in seen:
+            continue
+        seen.add(norm)
         for server, tools in servers.items():
             if normalize_skill_name(server) == norm:
                 return server, tools
     return None
🤖 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 `@src/pythinker_code/tools/skill/_mcp_bridge.py` around lines 37 - 67, The
candidate-extraction logic in find_mcp_server_for_skill_name duplicates
skill_lookup_keys; replace lines that build candidates (the raw/suffix/prefix
block) with a call to skill_lookup_keys(skill_name) to obtain the candidate
list, then iterate that list (normalizing with normalize_skill_name as before)
while using _index_mcp_servers(mcp_tools) to find a matching server; keep the
early return of server, tools and the seen dedup set logic if needed, and leave
_index_mcp_servers and normalize_skill_name calls intact.

Comment on lines +653 to +669
async def _simulate_exception_cleanup(session: Session | None) -> None:
"""Replicate exception-path cleanup from cli/__init__.py _reload_loop."""
import contextlib

if session is None:
return
with contextlib.suppress(Exception):
from pythinker_code.scratchpad import cleanup_session_scratch

await cleanup_session_scratch(
session.work_dir,
session_id=session.id,
session_title=session.title,
)
if session.is_empty():
await session.delete()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the test helper’s exception semantics aligned with CLI behavior.

At Line 667, _simulate_exception_cleanup deletes empty sessions without suppression, but the real CLI path suppresses deletion failures. This can produce false negatives in tests that are meant to mirror runtime behavior.

Proposed fix
 async def _simulate_exception_cleanup(session: Session | None) -> None:
@@
     if session.is_empty():
-        await session.delete()
+        with contextlib.suppress(Exception):
+            await session.delete()
🤖 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/core/test_session.py` around lines 653 - 669, The helper
_simulate_exception_cleanup currently calls await session.delete() unprotected,
diverging from the CLI which suppresses deletion failures; modify
_simulate_exception_cleanup so that the await session.delete() call is wrapped
in contextlib.suppress(Exception) (reuse the existing contextlib import) so both
cleanup_session_scratch and the session.delete() step suppress exceptions and
mirror the CLI behavior; keep checks using session.is_empty() and ensure no
other behavior changes.

Comment on lines +686 to +689
async def test_exception_cleanup_removes_session_scratch_file(
isolated_share_dir: Path,
work_dir: HostPath,
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve Ruff ARG001 on newly added test fixture parameters.

isolated_share_dir is unused in both new tests (Line 687 and Line 722), which triggers ARG001. Rename to _isolated_share_dir to keep fixture side effects while keeping lint clean.

Proposed fix
 async def test_exception_cleanup_removes_session_scratch_file(
-    isolated_share_dir: Path,
+    _isolated_share_dir: Path,
     work_dir: HostPath,
 ):
@@
 async def test_exception_cleanup_removes_scratch_even_for_nonempty_session(
-    isolated_share_dir: Path,
+    _isolated_share_dir: Path,
     work_dir: HostPath,
 ):

Also applies to: 721-724

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 687-687: Unused function argument: isolated_share_dir

(ARG001)

🤖 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/core/test_session.py` around lines 686 - 689, The ARG001 lint warning
is triggered because the test fixture parameter isolated_share_dir is unused;
rename the parameter to _isolated_share_dir in the
test_exception_cleanup_removes_session_scratch_file test to acknowledge the
fixture side effects without using it, and apply the same rename to the other
new test that also declares isolated_share_dir so both tests compile lint-clean
(update the function signatures only, e.g., def
test_exception_cleanup_removes_session_scratch_file(... isolated_share_dir:
Path, ...) -> ... to ... _isolated_share_dir: Path ...).

Source: Linters/SAST tools

Comment on lines +62 to +80
@pytest.mark.asyncio
async def test_recap_singular_on_persists_and_reloads(
runtime: Runtime, tmp_path: Path, monkeypatch
) -> None:
config_path = (tmp_path / "config.toml").resolve()
runtime.config.source_file = config_path
runtime.config.tui.turn_recaps = False
app = _make_shell_app(runtime, tmp_path)

config_for_save = get_default_config()
monkeypatch.setattr(shell_slash, "load_config", Mock(return_value=config_for_save))
monkeypatch.setattr(shell_slash, "save_config", Mock())
monkeypatch.setattr(shell_slash.console, "print", Mock())

with pytest.raises(Reload):
await _run_settings(app, "recap on")

assert config_for_save.tui.turn_recaps is True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the persistence call path, not only the mutated object.

This test currently passes if save_config(...) is skipped but config_for_save is mutated. Add explicit call assertions (matching the plural-form test) so alias behavior verifies persistence too.

Suggested test hardening
 `@pytest.mark.asyncio`
 async def test_recap_singular_on_persists_and_reloads(
     runtime: Runtime, tmp_path: Path, monkeypatch
 ) -> None:
@@
-    monkeypatch.setattr(shell_slash, "load_config", Mock(return_value=config_for_save))
-    monkeypatch.setattr(shell_slash, "save_config", Mock())
+    load_mock = Mock(return_value=config_for_save)
+    save_mock = Mock()
+    monkeypatch.setattr(shell_slash, "load_config", load_mock)
+    monkeypatch.setattr(shell_slash, "save_config", save_mock)
@@
     with pytest.raises(Reload):
         await _run_settings(app, "recap on")
 
+    load_mock.assert_called_once_with(config_path)
+    save_mock.assert_called_once_with(config_for_save, config_path)
     assert config_for_save.tui.turn_recaps is True

As per coding guidelines: “Flag tests missing assertions, or with trivially-true assertions.”

🤖 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/ui_and_conv/test_settings_recaps_slash.py` around lines 62 - 80, The
test mutates config_for_save but doesn't assert that the persistence path was
invoked; update test_recap_singular_on_persists_and_reloads to assert the save
call on the mocked save_config the same way the plural-form test does: verify
shell_slash.save_config was called (e.g., assert called_once) and that it was
invoked with the expected arguments (the config file
path/runtime.config.source_file and config_for_save) so the test ensures
persistence, not just in-memory mutation.

Source: Coding guidelines

Comment on lines +396 to +399
def render_agent_status(self, columns: int): # noqa: ARG002
return "TaskOutput(agent-reviewer · block, timeout 600s)"

def render_pinned_status_tail(self, columns: int): # noqa: ARG002

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optional: Add return type annotations to test stub methods.

Ruff flags missing return type annotations for the private methods in _BlockingTaskOutputDelegate. While this is a test stub, adding -> str would improve consistency:

-    def render_agent_status(self, columns: int):  # noqa: ARG002
+    def render_agent_status(self, columns: int) -> str:  # noqa: ARG002
         return "TaskOutput(agent-reviewer · block, timeout 600s)"

-    def render_pinned_status_tail(self, columns: int):  # noqa: ARG002
+    def render_pinned_status_tail(self, columns: int) -> str:  # noqa: ARG002
         return ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def render_agent_status(self, columns: int): # noqa: ARG002
return "TaskOutput(agent-reviewer · block, timeout 600s)"
def render_pinned_status_tail(self, columns: int): # noqa: ARG002
def render_agent_status(self, columns: int) -> str: # noqa: ARG002
return "TaskOutput(agent-reviewer · block, timeout 600s)"
def render_pinned_status_tail(self, columns: int) -> str: # noqa: ARG002
return ""
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 396-396: Missing return type annotation for private function render_agent_status

Add return type annotation: str

(ANN202)


[warning] 399-399: Missing return type annotation for private function render_pinned_status_tail

Add return type annotation: str

(ANN202)

🤖 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/ui_and_conv/test_visualize_running_prompt.py` around lines 396 - 399,
Add explicit return type annotations (-> str) to the test stub methods in class
_BlockingTaskOutputDelegate: change the signatures of render_agent_status(self,
columns: int) and render_pinned_status_tail(self, columns: int) to include ->
str so they read render_agent_status(self, columns: int) -> str and
render_pinned_status_tail(self, columns: int) -> str.

Source: Linters/SAST tools

@elkaix elkaix closed this Jun 13, 2026
@elkaix
elkaix deleted the feat/vis-dashboard-redesign branch June 13, 2026 13:37
@elkaix

elkaix commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

Superseded by #127 — this PR auto-closed when the branch was renamed from feat/vis-dashboard-redesign to feat/enhancements-and-bug-fixes. Same commits, continued there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant