Research – LSP Panel Integration (Dependency-Compliant)
Date: 2026-01-27 17:28:01 Owner: agent Phase: Research
Goal
Research how to add LSP status/diagnostics back to a UI panel after the refactor, while strictly following the dependency direction rule: ui → core → tools → utils/types.
Context
LSP was recently refactored (ticket tun-c2f8):
- Before: LSP lived in
src/tunacode/lsp/with status indicators in UI - After: LSP moved to
src/tunacode/tools/lsp/(tools-only concern) - Removed: LSP status indicator from
ResourceBar
The user wants to restore LSP status in the UI without violating dependency boundaries.
Findings
Current LSP Architecture
Location: src/tunacode/tools/lsp/
| File | Purpose |
|---|---|
__init__.py |
Public API: get_diagnostics(), format_diagnostics() |
client.py |
LSPClient class, Diagnostic dataclass |
servers.py |
Language server command mappings |
diagnostics.py |
maybe_prepend_lsp_diagnostics(), is_lsp_enabled() |
Current Integration:
write_file.pyandupdate_file.pycallmaybe_prepend_lsp_diagnostics()- Diagnostics are embedded in tool result as
<file_diagnostics>block - UI renderer (
ui/renderers/tools/diagnostics.py) parses and displays inline
Existing Panel Components
Status Displays:
ResourceBar(ui/widgets/resource_bar.py) - tokens, costs, modelStatusBar(ui/widgets/status_bar.py) - location, edited files, last action
LSP was previously in ResourceBar but was removed during refactor.
Dependency-Compliant Integration Patterns
The codebase uses 4 patterns for UI ← Core/Tools data flow:
Pattern 1: Callback Injection (Streaming, Tool Results)
# UI builds callback
def build_tool_result_callback(app: AppForCallbacks) -> ToolResultCallback:
def _callback(tool_name, status, args, result, duration_ms):
app.post_message(ToolResultDisplay(...))
return _callback
# UI passes to core
await process_request(..., tool_result_callback=callback)
# Core invokes callback (never imports UI)
callback(tool_name, status, args, result, duration_ms)
Pattern 2: Shared State via StateManager (ResourceBar)
# UI holds reference
self.state_manager: StateManager = state_manager
# Core/Tools write to state
state_manager.session.some_value = new_value
# UI reads from state
def _update_resource_bar(self) -> None:
session = self.state_manager.session
self.resource_bar.update_stats(tokens=session.total_tokens)
Pattern 3: Protocol-Based Decoupling
# UI defines protocol (repl_support.py)
class StatusBarLike(Protocol):
def update_last_action(self, tool_name: str) -> None: ...
class AppForCallbacks(Protocol):
status_bar: StatusBarLike
def post_message(self, message: ToolResultDisplay) -> bool: ...
# Core depends on protocol, not concrete class
Pattern 4: Textual Message Bus
# Callback posts message
app.post_message(ToolResultDisplay(...))
# Handler renders
def on_tool_result_display(self, message: ToolResultDisplay) -> None:
panel = tool_panel_smart(...)
self.chat.add_message(panel)
Recommended Integration Approach
Option A: StateManager Pattern (Recommended)
Cleanest approach - follows existing ResourceBar pattern.
Add LSP state to Session (
core/state.pyorcore/types.py):@dataclass class LSPStatus: enabled: bool = False last_check: datetime | None = None diagnostics_count: int = 0 server_running: bool = FalseTools write to state (
tools/lsp/diagnostics.py):def maybe_prepend_lsp_diagnostics(..., state_manager=None): # ... existing logic ... if state_manager: state_manager.session.lsp_status.diagnostics_count = len(diagnostics)UI reads from state (
ui/app.py):def _update_resource_bar(self) -> None: session = self.state_manager.session self.resource_bar.update_stats( tokens=session.total_tokens, lsp_enabled=session.lsp_status.enabled, lsp_diagnostics=session.lsp_status.diagnostics_count, )
Dependency flow: ui → core.state (clean)
Option B: Callback Pattern
More explicit, but adds threading through layers.
Add LSP callback type (
core/types.py):LSPStatusCallback = Callable[[bool, int], None] # (enabled, diagnostic_count)UI builds callback (
ui/repl_support.py):def build_lsp_status_callback(app: AppForCallbacks) -> LSPStatusCallback: def _callback(enabled: bool, count: int) -> None: app.resource_bar.update_lsp_status(enabled, count) return _callbackThread through process_request → agent → tool execution
Dependency flow: ui builds callback → core → tools invoke callback (clean but verbose)
Option C: Diagnostic Extraction from Tool Results
Minimal change - extract from existing data.
- UI extracts from tool result (
ui/app.py):def on_tool_result_display(self, message: ToolResultDisplay) -> None: # Extract diagnostics from result diagnostics = extract_diagnostics_from_result(message.result) if diagnostics: self.resource_bar.update_lsp_indicator(len(diagnostics.items))
Dependency flow: ui → ui.renderers.tools.diagnostics (internal, clean)
Key Patterns / Solutions Found
| Pattern | Use Case | Example Location |
|---|---|---|
| Callback injection | Real-time events | repl_support.py:147-174 |
| StateManager shared state | Persistent status | app.py:389-400 |
| Protocol decoupling | Type-safe contracts | repl_support.py:95-107 |
| Textual messages | Widget communication | widgets/messages.py |
Knowledge Gaps (Resolved)
StateManager threading - DoesANSWER: NO. Tools don't receiveStateManagerget passed to tool functions currently?state_manager. This rules out Option A without significant refactoring.LSP lifecycle - Updates per-file (when
write_file/update_filecomplete)ResourceBar capacity - TBD (visual design needed)
Performance - Not a concern with Option C (only updates on tool completion)
Recommendation: Option C (Extraction)
Why: Infrastructure already exists, no architectural changes needed.
Existing functions in ui/renderers/tools/diagnostics.py:
parse_diagnostics_block(result)→DiagnosticsDatawith countsextract_diagnostics_from_result(result)→ separates diagnostics from resultDiagnosticsData.error_count,.warning_count,.info_countalready computed
Implementation Checklist (Option C - Recommended)
Minimal changes, uses existing infrastructure:
ResourceBar (
ui/widgets/resource_bar.py):- Add
_lsp_errors: int = 0,_lsp_warnings: int = 0state - Add
update_lsp_status(errors: int, warnings: int)method - Update
_refresh_display()to show LSP indicator
- Add
App handler (
ui/app.py:on_tool_result_display()):- Import
parse_diagnostics_blockfromui/renderers/tools/diagnostics - Extract diagnostics:
data = parse_diagnostics_block(message.result) - If data:
self.resource_bar.update_lsp_status(data.error_count, data.warning_count)
- Import
Optional - Session tracking:
- Add
_session_lsp_errors: intto track cumulative - Reset on new session
- Add
Dependency flow: ui/app.py → ui/renderers/tools/diagnostics.py (same layer, clean)
References
Files:
src/tunacode/tools/lsp/- Current LSP implementationsrc/tunacode/ui/widgets/resource_bar.py- Target panelsrc/tunacode/ui/repl_support.py- Callback patternssrc/tunacode/core/state.py- StateManagersrc/tunacode/ui/renderers/tools/diagnostics.py- Existing diagnostic parsing
Tickets:
.tickets/tun-c2f8.md- LSP refactor epic (completed)
Documentation:
docs/lsp-diagnostics.md- User-facing LSP docsdocs/codebase-map/modules/lsp.md- LSP architecture