Skip to content

feat(tui+lsp): streaming polish, tool card renderers, and LSP implementation guard - #159

Merged
elkaix merged 14 commits into
mainfrom
feat/lsp-implementation-capability-guard
Jun 17, 2026
Merged

feat(tui+lsp): streaming polish, tool card renderers, and LSP implementation guard#159
elkaix merged 14 commits into
mainfrom
feat/lsp-implementation-capability-guard

Conversation

@elkaix

@elkaix elkaix commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

  • LSP: go_to_implementation returns a structured error when the server does not advertise implementationProvider; the client advertises implementation capability during initialize so servers like Pyright enable the provider automatically.
  • TUI streaming: Content blocks promote to scrollback once with paint-before-print in Rich Live mode; interrupted open ```report fences show a short note instead of raw JSON; paced transitions use bounded reveal; diff-based live refresh reduces flicker.
  • Tool card renderers: New card renderers for LSP, MCP resource, memory, read media, smart search, worktree, and background tasks; live diff visualization module; expanded renderer tests.

Test plan

  • make check-pythinker-code
  • make test-pythinker-code (focused UI/streaming/LSP tests)
  • Manual TUI smoke: streaming assistant text, tool cards, and live diff during a multi-tool turn

Summary by CodeRabbit

  • New Features

    • Added rich tool cards for LSP, Memory/Recall/Scratchpad, SmartSearch, ReadMediaFile, MCP resources, worktree actions, and background task cards.
    • Improved terminal UX with diff-based live rendering for smoother updates.
  • Bug Fixes

    • Prevented markdown/report code fence content from leaking during streaming previews.
    • Refined interactive streaming smoothness (less flicker, better scrollback behavior) and improved LSP go-to-implementation unsupported-server errors.
  • Documentation

    • Updated release notes, terminology, and reference-themed wording across the project.
  • Chores

    • Adjusted ignore/test collection to exclude reference scan outputs and debug logs.

…tationProvider

When a language server does not advertise implementationProvider in its
ServerCapabilities, calling go_to_implementation now returns a structured
error message (operation, server name, reason) instead of a raw exception.

Also advertises the `implementation` client capability in the LSP
initialize handshake so servers like Pyright enable the provider
automatically.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@elkaix, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 46 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80eb6823-ada4-4030-a245-9242ceffa08f

📥 Commits

Reviewing files that changed from the base of the PR and between 3f50717 and 877a763.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • src/pythinker_code/agents/default/system.md
  • src/pythinker_code/tools/agent/__init__.py
  • src/pythinker_code/ui/shell/components/report.py
  • src/pythinker_code/ui/shell/components/report_prose_blocks.py
  • src/pythinker_code/ui/shell/markdown/audit.py
  • src/pythinker_code/ui/shell/prompt.py
  • src/pythinker_code/ui/shell/spacing.py
  • src/pythinker_code/ui/shell/visualize/_blocks.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • tests/core/test_default_agent.py
  • tests/tools/test_agent_tool.py
  • tests/ui_and_conv/test_audit_report_rendering.py
  • tests/ui_and_conv/test_prompt_tips.py
  • tests/ui_and_conv/test_report.py
  • tests/ui_and_conv/test_report_prose_blocks.py
  • tests/ui_and_conv/test_stream_pacing.py
  • tests/ui_and_conv/test_streaming_content_block.py
  • tests/ui_and_conv/test_tui_card_tool_renderers.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
📝 Walkthrough

Walkthrough

The PR updates LSP capability handling and result metadata, adds several shell tool renderers and registry wiring, refactors streaming live-view behavior around reason-based flushing and DiffLive, and renames blackbox/reference wording and paths across docs, configs, and tests.

Changes

LSP Capability and Shell Tool Rendering

Layer / File(s) Summary
LSP instance and tool flow
src/pythinker_code/lsp/instance.py, src/pythinker_code/tools/lsp/tool.py
LspServerInstance exposes capabilities, initialize advertises implementation support, go_to_implementation short-circuits when unsupported, and result metadata now includes result_count, file_count, and operation.
LSP unsupported-server coverage
tests/tools/test_lsp_tool.py
The fake LSP server can omit implementationProvider, and the new test asserts the unsupported implementation path returns an error without sending the request.
New shell tool renderers
src/pythinker_code/ui/shell/tool_renderers/background.py, lsp.py, mcp_resource.py, memory.py, read_media.py, smart_search.py, worktree.py
Adds renderers for background task tools, LSP, MCP resources, memory tools, media reading, smart search, and worktree actions, including metadata parsing, expand/collapse behavior, and status summaries.
Renderer registry and coverage
src/pythinker_code/ui/shell/tool_renderers/__init__.py, tests/ui_and_conv/test_tui_card_tool_renderers.py, tests/ui_and_conv/test_tool_call_block.py
Registers the new renderers and expands UI tests for media, search, background tasks, memory tools, LSP, MCP resources, worktree metadata, and diff rendering behavior.

Streaming Live-View Overhaul

Layer / File(s) Summary
Content block pacing and preview suppression
src/pythinker_code/ui/shell/visualize/_blocks.py
Adds FlushReason, preview suppression for open report/code/diagram bodies, finalize sanitization, pacing limits, preview caching, row budgeting, and commit extraction on _ContentBlock.
DiffLive terminal renderer
src/pythinker_code/ui/shell/visualize/_diff_live.py
Adds a render hook that diffs successive frames and updates terminal rows in place while handling teardown and stream redirection.
Live view batching and reason-based flush
src/pythinker_code/ui/shell/visualize/_live_view.py
Switches to DiffLive, batches wire messages, adds incremental commit emission, and routes content flushes through FlushReason-aware transitions.
Prompt live-view incremental commits and budgets
src/pythinker_code/ui/shell/visualize/_interactive.py
Adjusts prompt refresh to emit incremental scrollback, reserve body budget, pin composing activity in the tail, and invalidate refreshes on forced flush.
Streaming tests
tests/ui_and_conv/test_streaming_content_block.py, tests/ui_and_conv/test_stream_pacing.py, tests/ui_and_conv/test_visualize_running_prompt.py
Adds coverage for fence suppression, preview caching, pacing limits, transition drains, incremental commits, and prompt flush ordering.

Terminology Cleanup, Scan Paths, and Guardrails

Layer / File(s) Summary
reference-scan path updates
.gitignore, pytest.ini, security-scan-findings.json
Moves ignored scan directories and finding paths from blackbox to reference-scan, and adds the new directory to pytest ignore rules.
Docs, comments, and changelog wording
CHANGELOG.md, packages/pythinker-review/..., plips/plip-10-lsp-system.md, src/pythinker_code/..., tests/...
Rewords docs, comments, docstrings, and test names from blackbox/pythinker-x terminology to reference, upstream, or bundled wording.
Static path guard and broadcast queue test
tests/test_ai_static_requirements.py, src/pythinker_code/ui/shell/prompt.py, tests/utils/test_broadcast_queue.py
Adds a source-file guard for /Users/ paths, precomputes prompt body rows before modal rendering, and verifies buffered publish_nowait delivery order.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Pythoughts-labs/pythinker-code#62: Directly overlaps with the streaming live-view and paced reveal changes in src/pythinker_code/ui/shell/visualize/_blocks.py and related TUI rendering paths.
  • Pythoughts-labs/pythinker-code#157: Shares the same LSP subsystem surface (src/pythinker_code/lsp/instance.py, src/pythinker_code/tools/lsp/tool.py) that now adds capability gating and result metadata handling.

Suggested labels

enhancement

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lsp-implementation-capability-guard

elkaix added 2 commits June 17, 2026 00:14
Content blocks now promote to scrollback exactly once with a paint-before-print
step in Rich Live mode, interrupted open ```report``` fences surface a short
note instead of raw JSON, and paced transitions use bounded reveal so large
backlogs no longer dump in full before tool cards.

- src/pythinker_code/ui/shell/prompt.py: 78 lines
- src/pythinker_code/ui/shell/visualize/_blocks.py: 369 lines
- src/pythinker_code/ui/shell/visualize/_interactive.py: 148 lines
- src/pythinker_code/ui/shell/visualize/_live_view.py: 98 lines
- tests/ui_and_conv/test_stream_pacing.py: 170 lines
- tests/ui_and_conv/test_streaming_content_block.py: 653 lines
- tests/ui_and_conv/test_visualize_running_prompt.py: 166 lines
- tests/utils/test_broadcast_queue.py: 12 lines
- tasks/streaming-render-rootcause.md: rootcause report (new)
- CHANGELOG.md: Unreleased entry
@elkaix elkaix changed the title fix(lsp): guard go_to_implementation against servers without implementationProvider feat(tui+lsp): streaming polish, tool card renderers, and LSP implementation guard Jun 17, 2026
Comment thread src/pythinker_code/ui/shell/prompt.py Fixed
Comment thread tests/ui_and_conv/test_visualize_running_prompt.py
Comment thread src/pythinker_code/ui/shell/visualize/_diff_live.py
Comment thread src/pythinker_code/ui/shell/visualize/_live_view.py Fixed

@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: 11

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/utils/rich/syntax.py (1)

280-293: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the accepted-theme contract.

available_code_themes() does not include custom .tmTheme names; those are only added by list_picker_code_themes(). The docstring currently implies otherwise.

Suggested wording
-    """Accepted ``code_theme`` values: bundled Pythinker names, sentinels, custom, Pygments."""
+    """Accepted ``code_theme`` values: bundled Pythinker names, sentinels, and Pygments styles."""
🤖 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/utils/rich/syntax.py` around lines 280 - 293, The
docstring for the available_code_themes() function is misleading because it
implies that custom themes are included in the returned list, but the actual
implementation only returns bundled Pythinker theme names, Catppuccin and
Pythinker ANSI sentinels, and Pygments styles. Update the docstring to
accurately document what theme sources are actually included and clarify that
custom .tmTheme names are NOT included in this function but are only added
separately by the list_picker_code_themes() function.
🤖 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/tools/lsp/tool.py`:
- Around line 102-106: The truthiness check for implementationProvider at line
105 incorrectly rejects empty dictionaries because empty dicts evaluate as
falsy. Replace the `not server.capabilities.implementationProvider` check with
explicit comparisons that properly handle all LSP spec cases: check if
implementationProvider is explicitly False or is None, rather than relying on
truthiness evaluation. This ensures that empty dicts (which indicate supported
with default options) are correctly accepted while unsupported (False) and
unadvertised (None) cases are properly rejected.

In `@src/pythinker_code/ui/shell/tool_renderers/lsp.py`:
- Around line 63-70: The final return statement in the _operation_detail
function directly accesses ctx.args.get("operation") without checking if
ctx.args is None first, which causes an AttributeError in the result-only render
path. Guard ctx.args against None before attempting to call the get method on it
in the return statement, ensuring safe fallback behavior to "result". Use the
same guarded access pattern applied elsewhere in this file for similar attribute
access scenarios.

In `@src/pythinker_code/ui/shell/tool_renderers/worktree.py`:
- Around line 44-69: Both the _render_enter_result and _render_exit_result
functions must check the result.is_error property before rendering success
header messages to prevent failed tool calls from appearing as successful. Add a
check at the beginning of each function that returns early (with None or an
appropriate error rendering) if result.is_error is True, ensuring that error
states are honored before any success-oriented messages like "Switched to
worktree" or "Kept/Removed worktree" are rendered to the user.

In `@src/pythinker_code/ui/shell/visualize/_blocks.py`:
- Around line 255-276: The current code skips bare fence openers (fences without
language info) because of the "if not info: continue" check, preventing proper
tracking of unclosed plain fences. Instead of immediately returning when a
language-tagged fence is found, track fence pairs by storing the first opening
fence's details (open_match, open_marker, open_info, open_is_report) and look
for matching closing fences where the marker matches and info is empty. After
the loop, process the tracked opening fence if it was found and is not a report
fence, extracting the language from open_info and building the preview
placeholder using open_match's position instead of the current immediate return
logic.

In `@src/pythinker_code/ui/shell/visualize/_diff_live.py`:
- Around line 147-152: The _stop_interactive() method in the DiffLive class
calls self.console.clear_live() unconditionally before checking if the instance
is nested. When this instance is nested (self._nested is True), it should not
own the console._live and therefore should not clear it, as this tears down the
parent live renderer. Move the self.console.clear_live() call to only execute
when self._nested is False by either placing it after the nested guard check or
wrapping it with a conditional that skips it when nested.

In `@src/pythinker_code/ui/shell/visualize/_interactive.py`:
- Around line 356-367: The TurnEnd message handling block sets _force_refresh to
True and calls _flush_prompt_refresh(), but this method does not actually
invalidate the prompt state unless _dirty or _need_recompose is already True.
For contentless or tool-only turns, the prompt stays stale after TurnEnd. To fix
this, when handling the TurnEnd message and setting _force_refresh = True before
the _flush_prompt_refresh() call, also set either _dirty or _need_recompose to
True so that the refresh actually gets processed and the prompt state is
properly invalidated.

In `@src/pythinker_code/ui/shell/visualize/_live_view.py`:
- Around line 353-376: The `_extend_wire_batch()` method creates a new
`wire.receive()` task during batch collection, but if that task raises
`QueueShutDown`, the exception bypasses dispatch of messages already accumulated
in the `messages` list, potentially dropping critical final messages like
`TurnEnd` or `ToolResult`. Catch the `QueueShutDown` exception within
`_extend_wire_batch()` and either return a flag indicating the wire is closed or
handle it such that the caller can dispatch the accumulated `messages` before
running shutdown cleanup, ensuring no buffered messages are lost. Apply the same
fix to the similar code pattern around line 518-520.

In `@tests/tools/test_lsp_tool.py`:
- Around line 490-507: The test_go_to_implementation_unsupported_server function
currently only validates the error message content but lacks verification that
the textDocument/implementation request was never sent to the LSP server. Add a
log assertion or message capture mechanism to confirm that the guard
short-circuits before dispatching the request when the implementationProvider
capability is missing, ensuring the test covers the observable effect of the
guard preventing the request rather than just message matching.

In `@tests/ui_and_conv/test_stream_pacing.py`:
- Around line 146-165: Remove the monkeypatching of the private
_find_committed_boundary function and the assertion on the calls counter in the
test_reveal_tick_skips_markdown_boundary_scan_without_newline function. Instead
of verifying internal call counts, assert on externally observable behavior such
as the revealed or committed state of the _ContentBlock after calling
reveal_tick multiple times, or verify the actual output/content that gets
emitted from the block.

In `@tests/ui_and_conv/test_tui_card_tool_renderers.py`:
- Around line 2185-2217: The test function
test_worktree_renderers_parse_tool_output_metadata currently only validates the
success paths for the EnterWorktree and ExitWorktree renderers. Add failure-path
test coverage by creating additional _render calls with is_error=True parameter
for both EnterWorktree and ExitWorktree (similar to the existing success cases).
For each error case, add assertions that verify error text is displayed in the
output and that the success-path labels (such as "Switched to worktree" for
EnterWorktree and "Kept worktree" for ExitWorktree) are NOT present, ensuring
the error paths are properly handled and displayed.

In `@tests/ui_and_conv/test_visualize_running_prompt.py`:
- Around line 897-942: The test
test_prompt_live_view_flushes_content_before_marking_turn_ended does not
actually verify the ordering constraint stated in its name. Currently it only
checks the final state (that view._turn_ended is True and
view._current_content_block is None), but does not prove that flush_content was
called while view._turn_ended was still False. Add a mechanism to track when
flush_content is called (using monkeypatch or similar) and capture the state of
view._turn_ended at that moment, then add an explicit assertion that verifies
flush_content ran before view._turn_ended became True. This will properly test
the ordering guarantee implied by the test's name.

---

Outside diff comments:
In `@src/pythinker_code/utils/rich/syntax.py`:
- Around line 280-293: The docstring for the available_code_themes() function is
misleading because it implies that custom themes are included in the returned
list, but the actual implementation only returns bundled Pythinker theme names,
Catppuccin and Pythinker ANSI sentinels, and Pygments styles. Update the
docstring to accurately document what theme sources are actually included and
clarify that custom .tmTheme names are NOT included in this function but are
only added separately by the list_picker_code_themes() function.
🪄 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: c4d12e7d-0138-4a42-8ebf-a8a889b2f1a0

📥 Commits

Reviewing files that changed from the base of the PR and between 7041b2c and 9cf43bf.

⛔ Files ignored due to path filters (10)
  • docs/en/customization/architecture.md is excluded by !docs/**
  • docs/en/release-notes/changelog.md is excluded by !docs/**
  • docs/history/CHANGELOG-pre-0.8.0.md is excluded by !docs/**
  • tasks/agent-harness-adoption-plan.md is excluded by !tasks/**
  • tasks/design-adoption-blueprint.md is excluded by !tasks/**
  • tasks/lessons.md is excluded by !tasks/**
  • tasks/reference-port-status.md is excluded by !tasks/**
  • tasks/streaming-render-rootcause.md is excluded by !tasks/**
  • tasks/streaming-wire-bug-hunt-report.md is excluded by !tasks/**
  • tasks/todo.md is excluded by !tasks/**
📒 Files selected for processing (58)
  • .gitignore
  • CHANGELOG.md
  • packages/pythinker-review/README.md
  • packages/pythinker-review/docs/code-reviewr-migration.md
  • packages/pythinker-review/docs/reference-parity.md
  • packages/pythinker-review/docs/security-scan-migration.md
  • packages/pythinker-review/src/pythinker_review/engine/structured_diff.py
  • packages/pythinker-review/src/pythinker_review/security_intel/__init__.py
  • packages/pythinker-review/src/pythinker_review/security_scan/knowledge.py
  • plips/plip-10-lsp-system.md
  • pytest.ini
  • security-scan-findings.json
  • src/pythinker_code/agents/default/security_reviewer.yaml
  • src/pythinker_code/llm.py
  • src/pythinker_code/lsp/instance.py
  • src/pythinker_code/plugin/marketplace.py
  • src/pythinker_code/tools/lsp/tool.py
  • src/pythinker_code/ui/shell/__init__.py
  • src/pythinker_code/ui/shell/components/dynamic_border.py
  • src/pythinker_code/ui/shell/components/render_utils.py
  • src/pythinker_code/ui/shell/components/tool_execution.py
  • src/pythinker_code/ui/shell/motion.py
  • src/pythinker_code/ui/shell/prompt.py
  • src/pythinker_code/ui/shell/slash.py
  • src/pythinker_code/ui/shell/tool_renderers/__init__.py
  • src/pythinker_code/ui/shell/tool_renderers/_file_diff.py
  • src/pythinker_code/ui/shell/tool_renderers/_render_utils.py
  • src/pythinker_code/ui/shell/tool_renderers/background.py
  • src/pythinker_code/ui/shell/tool_renderers/grep.py
  • src/pythinker_code/ui/shell/tool_renderers/lsp.py
  • src/pythinker_code/ui/shell/tool_renderers/mcp_resource.py
  • src/pythinker_code/ui/shell/tool_renderers/memory.py
  • src/pythinker_code/ui/shell/tool_renderers/read.py
  • src/pythinker_code/ui/shell/tool_renderers/read_media.py
  • src/pythinker_code/ui/shell/tool_renderers/smart_search.py
  • src/pythinker_code/ui/shell/tool_renderers/worktree.py
  • src/pythinker_code/ui/shell/tool_renderers/write.py
  • src/pythinker_code/ui/shell/update.py
  • src/pythinker_code/ui/shell/visualize/_blocks.py
  • src/pythinker_code/ui/shell/visualize/_diff_live.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • src/pythinker_code/ui/shell/visualize/_live_view.py
  • src/pythinker_code/ui/theme/pythinker_themes.py
  • src/pythinker_code/utils/rich/syntax.py
  • tests/core/test_toolset_concurrency.py
  • tests/test_ai_static_requirements.py
  • tests/tools/test_lsp_tool.py
  • tests/ui_and_conv/test_modal_lifecycle.py
  • tests/ui_and_conv/test_pythinker_themes_port.py
  • tests/ui_and_conv/test_shell_slash_commands.py
  • tests/ui_and_conv/test_spinner_words.py
  • tests/ui_and_conv/test_stream_pacing.py
  • tests/ui_and_conv/test_streaming_content_block.py
  • tests/ui_and_conv/test_tool_call_block.py
  • tests/ui_and_conv/test_tool_search_suppression.py
  • tests/ui_and_conv/test_tui_card_tool_renderers.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
  • tests/utils/test_broadcast_queue.py

Comment thread src/pythinker_code/tools/lsp/tool.py Outdated
Comment thread src/pythinker_code/ui/shell/tool_renderers/lsp.py Outdated
Comment thread src/pythinker_code/ui/shell/tool_renderers/worktree.py
Comment thread src/pythinker_code/ui/shell/visualize/_blocks.py Outdated
Comment thread src/pythinker_code/ui/shell/visualize/_diff_live.py
Comment thread src/pythinker_code/ui/shell/visualize/_live_view.py Outdated
Comment thread tests/tools/test_lsp_tool.py
Comment thread tests/ui_and_conv/test_stream_pacing.py Outdated
Comment thread tests/ui_and_conv/test_tui_card_tool_renderers.py
Comment thread tests/ui_and_conv/test_visualize_running_prompt.py
elkaix added 7 commits June 17, 2026 13:36
Route interactive-mode scrollback through run_in_terminal so prompt
preamble is not fossilized into permanent transcript output. Render
parent-bullet summaries with aligned field rows as structured blocks
with preserved hierarchy and correct continuation wrap indent. Make ty
blocking for pythinker-code checks and accept TodoWrite merge field.
Share the approval/pager PythinkerSyntax pipeline with Edit/Write tool cards
while keeping the compact boxless layout. Add AGENTS.md guidance to prefer LSP
when a language server is available.
Render tool-card diffs in a three-column grid so wrapped continuation rows
stay under the code body and repeat +/- markers. Fix LSP implementationProvider
empty-object support, worktree error surfacing, nested DiffLive teardown, wire
shutdown batching, and unclosed fence preview suppression.
The fake LSP server only creates lsp.log when a loggable method arrives.
When the implementationProvider guard fires first, no loggable methods are
dispatched so the file is legitimately absent.  Reading it unconditionally
raised FileNotFoundError.
- update SetTodoList schema snapshot to include the new `merge`
  compatibility field (excluded from serialization but present in JSON schema)
- rename and flip test_helper_leaves_bare_fence_line_untouched: bare
  ` ``` ` openers are now suppressed with fallback lang "code", matching
  the paired-fence tracking introduced alongside the CodeRabbit fix
- patch DiffLive (not the removed Rich Live) in
  test_live_view_processes_external_approval_messages; drop the
  Rich-specific _live_render attribute and add refresh() no-op
- add CHANGELOG unreleased entry for the smooth-streaming coalescing work

@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: 2

Caution

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

⚠️ Outside diff range comments (3)
src/pythinker_code/ui/shell/markdown/normalizers.py (1)

476-477: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include - in the fast-path guard for report-block normalization.

The parser now accepts parent bullets matching [-•], but the early return still exits when is absent. Hyphen-only inputs skip normalization entirely.

Suggested fix
 def normalize_space_aligned_report_blocks(markup: str) -> str:
     """Convert LLM space-column report rows into nested Markdown lists."""
-    if "•" not in markup:
+    if "•" not in markup and "-" not in markup:
         return markup

Also applies to: 508-513

🤖 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/ui/shell/markdown/normalizers.py` around lines 476 - 477,
The fast-path guard condition that checks if "•" is not present in markup needs
to be updated to also check for the hyphen character "-" since the parser now
accepts both as valid parent bullets matching the pattern [-•]. Update the
condition in the early return at lines 476-477 to return only when both "•" and
"-" are absent from the markup, ensuring that hyphen-only inputs do not skip
normalization. Apply the same fix to the other location mentioned at lines
508-513.
tests/ui_and_conv/test_btw.py (1)

882-904: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the new queued steer echo side effect.

This test now initializes _pending_scrollback, but it still only verifies steering and the dedup counter. A regression that stops queuing the permanent Ctrl+S echo would pass. Add an assertion for one queued non-blank-row echo entry, or flush and assert the captured output. As per coding guidelines, tests should not cover only the happy path while ignoring changed edge/user-visible behavior.

Proposed assertion
         assert len(steered) == 1
         assert view._pending_local_steer_count == 1  # counter incremented
+        assert len(view._pending_scrollback) == 1
+        _, blank_row = view._pending_scrollback[0]
+        assert blank_row is False
🤖 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_btw.py` around lines 882 - 904, The
test_normal_text_via_ctrl_s_steers_normally test initializes _pending_scrollback
as an empty list but never verifies that the handle_immediate_steer method
queues an echo entry to it. After the existing assertions for steered and
_pending_local_steer_count, add an assertion to verify that _pending_scrollback
now contains exactly one entry representing the queued echo for the Ctrl+S
command. This ensures the test covers the side effect of queuing the permanent
echo and would catch regressions if that behavior stops working.

Source: Coding guidelines

src/pythinker_code/ui/shell/visualize/_interactive.py (1)

298-302: ⚠️ Potential issue | 🟠 Major

Add non-terminal fallback to incremental scrollback emission.

_emit_incremental_content_commits always calls run_in_terminal, which does not write to captured stdout in non-terminal/piped mode. Since take_committed_renderables() removes these items before printing, they are silently lost in piped sessions. _flush_pending_scrollback already handles this with a console.is_terminal check (lines 328–331); apply the same pattern here.

-        await run_in_terminal(emit_committed)
+        if console.is_terminal:
+            await run_in_terminal(emit_committed)
+        else:
+            emit_committed()
🤖 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/ui/shell/visualize/_interactive.py` around lines 298 -
302, The emit_committed function inside _emit_incremental_content_commits always
uses run_in_terminal, which fails to write to captured stdout in
piped/non-terminal mode, causing renderables to be silently lost. Add a
console.is_terminal check before calling run_in_terminal (similar to the pattern
already implemented in _flush_pending_scrollback at lines 328-331), and provide
a fallback that directly emits the renderables when not in terminal mode,
ensuring committed items are properly output in both terminal and piped
sessions.
🤖 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/ui/shell/visualize/_interactive.py`:
- Around line 309-331: The _flush_pending_scrollback() method clears
_pending_scrollback before awaiting the run_in_terminal(emit) call, which can
cause concurrent flushes or cancellation to silently drop or reorder queued
transcript entries. Add a lock to serialize pending scrollback flushes and move
the clearing of _pending_scrollback to after the emit operation completes
successfully, rather than before. This ensures that the to_print list is only
populated and the queue is only cleared after the terminal handoff finishes,
preventing any concurrent calls from _status_refresh_loop() or visualize_loop()
from causing data loss.

In `@tests/ui_and_conv/test_tui_card_tool_renderers.py`:
- Around line 1083-1091: The test function
test_render_diff_without_path_does_not_construct_highlighter is patching
make_diff_highlighter in the wrong location. Currently it patches
pythinker_code.utils.rich.diff_render.make_diff_highlighter, but the render_diff
function imports and uses make_diff_highlighter from
pythinker_code.ui.shell.components.diff. Update the monkeypatch call to target
the make_diff_highlighter symbol where it is actually imported and used by
render_diff, which is in the pythinker_code.ui.shell.components.diff module, not
in the original source module.

---

Outside diff comments:
In `@src/pythinker_code/ui/shell/markdown/normalizers.py`:
- Around line 476-477: The fast-path guard condition that checks if "•" is not
present in markup needs to be updated to also check for the hyphen character "-"
since the parser now accepts both as valid parent bullets matching the pattern
[-•]. Update the condition in the early return at lines 476-477 to return only
when both "•" and "-" are absent from the markup, ensuring that hyphen-only
inputs do not skip normalization. Apply the same fix to the other location
mentioned at lines 508-513.

In `@src/pythinker_code/ui/shell/visualize/_interactive.py`:
- Around line 298-302: The emit_committed function inside
_emit_incremental_content_commits always uses run_in_terminal, which fails to
write to captured stdout in piped/non-terminal mode, causing renderables to be
silently lost. Add a console.is_terminal check before calling run_in_terminal
(similar to the pattern already implemented in _flush_pending_scrollback at
lines 328-331), and provide a fallback that directly emits the renderables when
not in terminal mode, ensuring committed items are properly output in both
terminal and piped sessions.

In `@tests/ui_and_conv/test_btw.py`:
- Around line 882-904: The test_normal_text_via_ctrl_s_steers_normally test
initializes _pending_scrollback as an empty list but never verifies that the
handle_immediate_steer method queues an echo entry to it. After the existing
assertions for steered and _pending_local_steer_count, add an assertion to
verify that _pending_scrollback now contains exactly one entry representing the
queued echo for the Ctrl+S command. This ensures the test covers the side effect
of queuing the permanent echo and would catch regressions if that behavior stops
working.
🪄 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: e6378ca1-aed1-4dfb-bbee-628d590c45f2

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf43bf and 3f50717.

📒 Files selected for processing (32)
  • AGENTS.md
  • CHANGELOG.md
  • Makefile
  • src/pythinker_code/tools/lsp/tool.py
  • src/pythinker_code/tools/todo/__init__.py
  • src/pythinker_code/ui/shell/components/diff.py
  • src/pythinker_code/ui/shell/components/report.py
  • src/pythinker_code/ui/shell/components/report_prose_blocks.py
  • src/pythinker_code/ui/shell/markdown/normalizers.py
  • src/pythinker_code/ui/shell/prompt.py
  • src/pythinker_code/ui/shell/tool_renderers/_file_diff.py
  • src/pythinker_code/ui/shell/tool_renderers/edit.py
  • src/pythinker_code/ui/shell/tool_renderers/lsp.py
  • src/pythinker_code/ui/shell/tool_renderers/worktree.py
  • src/pythinker_code/ui/shell/tool_renderers/write.py
  • src/pythinker_code/ui/shell/visualize/_blocks.py
  • src/pythinker_code/ui/shell/visualize/_diff_live.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • src/pythinker_code/ui/shell/visualize/_live_view.py
  • src/pythinker_code/utils/rich/diff_render.py
  • tests/tools/test_lsp_tool.py
  • tests/tools/test_todo.py
  • tests/tools/test_tool_schemas.py
  • tests/ui_and_conv/test_btw.py
  • tests/ui_and_conv/test_modal_lifecycle.py
  • tests/ui_and_conv/test_output_guards.py
  • tests/ui_and_conv/test_report_prose_blocks.py
  • tests/ui_and_conv/test_stream_pacing.py
  • tests/ui_and_conv/test_streaming_content_block.py
  • tests/ui_and_conv/test_tui_card_tool_renderers.py
  • tests/ui_and_conv/test_tui_streaming_phase0.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
💤 Files with no reviewable changes (1)
  • src/pythinker_code/ui/shell/prompt.py

Comment thread src/pythinker_code/ui/shell/visualize/_interactive.py Outdated
Comment thread tests/ui_and_conv/test_tui_card_tool_renderers.py
elkaix added 3 commits June 17, 2026 15:25
…port rendering

Streaming smoothness (the agent-working "jump"):
- Stop committing completed prose to scrollback mid-stream. Each mid-stream
  commit was a run_in_terminal prompt-app teardown — the visible jump. Prose now
  stays in the in-place live preview (tail-clamped by _compose_composing) and is
  flushed to scrollback exactly once at a tool transition or turn end, mirroring
  the reference in-place-then-flush architecture.
- Remove the now-dead per-tick commit throttle (_maybe_emit_incremental_commits
  and the _INCREMENTAL_COMMIT_* constants); the paced-reveal cap stays.

Finalize/clipping honesty:
- Route scrollback handoffs through _run_scrollback_handoff and show a
  Finalizing state instead of a false idle prompt while scrollback is pending.
- Surface an "earlier output hidden · Ctrl+O expand" marker when live rows are
  clamped, instead of silently dropping them.

Agent tool / prompts:
- Add a fail-loud "did you mean?" suggestion for hallucinated subagent type
  names (cross-harness aliases + fuzzy match); never substitute silently.
- Document structured report findings blocks and dual terminal/saved report
  destinations; expand report and audit-markdown rendering.

Tests updated to the new contracts; full check + tests + tests_e2e green.
- Collapse low/info severities in large compact reports with a saved-report pointer
- Strip duplicate preamble/trailer prose and compact artifact footer paths
- Refine agent prompt: report JSON or prose (not both), compact terminal footers
- Fix prose-block inline-code wrapping and add handoff-trace diagnostics
- Harden tests for worktree errors, turn-end flush ordering, and audit rendering
Anchor tool-output path collapsing on the .pythinker/sessions marker instead
of the runtime share dir so compact_known_paths works across users and OSes.
Comment thread src/pythinker_code/ui/shell/visualize/_interactive.py Fixed
Comment thread src/pythinker_code/ui/shell/components/report.py Fixed
Comment thread src/pythinker_code/ui/shell/components/report.py Fixed
Comment thread src/pythinker_code/ui/shell/components/report.py Fixed
…rden review nits

- Update prompt phrase-pin test to the reworded findings-report contract in
  system.md (deliberate rewording in 6302b49; fix the test, not the prompt).
- Patch make_diff_highlighter where render_diff resolves it
  (ui.shell.components.diff), so the no-highlighter-without-path test actually
  guards the contract.
- Remove the orphaned _COMPACT_REPORT_* threshold constants left unused after
  the layout switch to literal thresholds; behavior unchanged.
- Document the intentional best-effort OSError swallow in _handoff_trace.
@elkaix
elkaix merged commit 2313fb1 into main Jun 17, 2026
40 checks passed
@elkaix
elkaix deleted the feat/lsp-implementation-capability-guard branch June 17, 2026 20:38
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