ba3eb0c090
Phase 6: Eliminate Optional[T] returns - BATCH 2 of 7
Before: 7 more Optional[T] returns removed
After: 0 in command_palette.py, diff_viewer.py, fuzzy_anchor.py,
multi_agent_conductor.py, patch_modal.py, app_controller.py
Delta: -7 sites (cumulative: -15 of 30)
Specific changes:
- src/command_palette.py:50: CommandRegistry.get() returns Command (zero-init
sentinel: id="", title="", category="uncategorized", action=lambda: None)
- src/diff_viewer.py:117: get_line_color returns "" when no marker prefix
- src/fuzzy_anchor.py:40: FuzzyAnchor.resolve_slice returns (-1, -1) sentinel
(replaced 3x `return None` with `return (-1, -1)`)
- src/multi_agent_conductor.py:64: WorkerPool.spawn returns threading.Thread()
(empty sentinel, not started) when pool is full
- src/patch_modal.py:33: PatchModalManager.get_pending_patch returns
PendingPatch; class has EMPTY_PATCH sentinel; field type changed from
Optional[PendingPatch] to PendingPatch; 2x `= None` reset replaced with
`= EMPTY_PATCH`
- src/app_controller.py:4414: _confirm_and_run returns "" when not approved
(was Optional[str] returning None)
Test updates:
- tests/test_diff_viewer.py:95: get_line_color(" context") == ""
- tests/test_fuzzy_anchor.py:42,59: assert result == (-1, -1)
- tests/test_parallel_execution.py:31: t3 sentinel is now unstarted thread
(check via not t3.is_alive())
- tests/test_patch_modal.py:9,31,78: get_pending_patch() == "" sentinel check
Verification:
- audit_weak_types --strict: OK (107 <= 112 baseline)
- 22+ tests pass (test_diff_viewer, test_fuzzy_anchor,
test_parallel_execution, test_patch_modal, test_command_palette)
- py_check_syntax: OK on all changed files
REMAINING: ~15 Optional[T] returns in:
- src/external_editor.py (3)
- src/file_cache.py (7)
- src/diff_viewer.py: parse_hunk_header (1)
- src/models.py: ExternalEditorConfig.get_default (1)
- src/project_manager.py: load_track_state (1)
- src/session_logger.py: log_tool_call (1)
- src/app_controller.py: _pending_mma_spawn, _pending_mma_approval (2)
209 lines
7.0 KiB
Python
209 lines
7.0 KiB
Python
from __future__ import annotations
|
|
|
|
from imgui_bundle import imgui
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional, Callable, List, Dict, Any
|
|
|
|
from src.result_types import ErrorInfo, ErrorKind, Result
|
|
|
|
|
|
|
|
@dataclass
|
|
class Command:
|
|
id: str
|
|
title: str
|
|
category: str
|
|
shortcut: Optional[str] = None
|
|
description: str = ""
|
|
enabled_when: Optional[str] = None
|
|
action: Optional[Callable] = None
|
|
|
|
@dataclass
|
|
class ScoredCommand:
|
|
command: Command
|
|
score: float
|
|
|
|
|
|
class CommandRegistry:
|
|
def __init__(self) -> None:
|
|
self._commands: Dict[str, Command] = {}
|
|
|
|
def register(self, command_or_callable: Any) -> Any:
|
|
if isinstance(command_or_callable, Command):
|
|
cmd = command_or_callable
|
|
else:
|
|
cmd = Command(
|
|
id=command_or_callable.__name__,
|
|
title=command_or_callable.__name__.replace("_", " ").title(),
|
|
category="uncategorized",
|
|
action=command_or_callable,
|
|
)
|
|
if cmd.id in self._commands:
|
|
raise ValueError(f"Command {cmd.id} already registered")
|
|
self._commands[cmd.id] = cmd
|
|
return command_or_callable
|
|
|
|
def all(self) -> List[Command]:
|
|
return list(self._commands.values())
|
|
|
|
def get(self, command_id: str) -> Command:
|
|
return self._commands.get(command_id) or Command(id="", title="", category="uncategorized", action=lambda: None)
|
|
|
|
|
|
def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]:
|
|
query_lower = query.lower()
|
|
scored: List[ScoredCommand] = []
|
|
for cmd in candidates:
|
|
title_lower = cmd.title.lower()
|
|
if not _is_subsequence(query_lower, title_lower):
|
|
continue
|
|
score = _compute_score(query_lower, title_lower)
|
|
scored.append(ScoredCommand(command=cmd, score=score))
|
|
scored.sort(key=lambda r: r.score, reverse=True)
|
|
return scored[:top_n]
|
|
|
|
|
|
def _is_subsequence(query: str, target: str) -> bool:
|
|
qi = 0
|
|
for ch in target:
|
|
if qi < len(query) and ch == query[qi]:
|
|
qi += 1
|
|
return qi == len(query)
|
|
|
|
|
|
def _compute_score(query: str, target: str) -> float:
|
|
score = 0.0
|
|
if target.startswith(query): score += 1.0
|
|
elif _starts_at_word_boundary(query, target): score += 0.5
|
|
if _is_contiguous(query, target): score += 0.3
|
|
gaps = _count_gaps(query, target)
|
|
score -= 0.1 * gaps
|
|
return score
|
|
|
|
|
|
def _starts_at_word_boundary(query: str, target: str) -> bool:
|
|
if not target.startswith(query):
|
|
return False
|
|
return len(query) == 0 or not query[0].isalnum() or len(target) == len(query) or not target[len(query)].isalnum()
|
|
|
|
|
|
def _is_contiguous(query: str, target: str) -> bool:
|
|
return query in target
|
|
|
|
|
|
def _count_gaps(query: str, target: str) -> int:
|
|
qi = 0
|
|
gaps = 0
|
|
last_match = -1
|
|
for ti, ch in enumerate(target):
|
|
if qi < len(query) and ch == query[qi]:
|
|
if last_match >= 0 and ti - last_match > 1: gaps += ti - last_match - 1
|
|
last_match = ti
|
|
qi += 1
|
|
return gaps
|
|
|
|
|
|
def _close_palette(app: Any) -> None:
|
|
"""Close the palette and reset all per-open state."""
|
|
app.show_command_palette = False
|
|
app._command_palette_query = ""
|
|
app._command_palette_selected = 0
|
|
app._command_palette_focused = False
|
|
app._command_palette_input_focused = False
|
|
|
|
|
|
def _execute(app: Any, command: Command) -> None:
|
|
"""Run a command and close the palette. Catches exceptions to keep the modal clean."""
|
|
if not command.action:
|
|
return
|
|
try:
|
|
command.action(app)
|
|
except (AttributeError, TypeError, ValueError, OSError) as e:
|
|
_cmd_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Action {command.id} raised: {e}", source="command_palette._execute", original=e)])
|
|
print(f"[CommandPalette] Action {command.id} raised: {e}")
|
|
_close_palette(app)
|
|
|
|
|
|
def render_palette_modal(app: Any, commands: List[Command]) -> None:
|
|
"""Renders the interactive Command Palette modal. Exposes a text input query bar
|
|
and lists matching commands with fuzzy matching, supporting keyboard navigation (Up/Down/Enter/Esc).
|
|
|
|
SSDL: `[I:query_input] -> [B:results_list] => [B:execute_command]`
|
|
|
|
ASCII Layout Map:
|
|
+==================== Command Palette ===================+
|
|
| |query_text| |
|
|
| +-----------------------------------------------------+ |
|
|
| | > [category-a] Command Title A (selected) | |
|
|
| | [category-b] Command Title B | |
|
|
| +-----------------------------------------------------+ |
|
|
+========================================================+
|
|
"""
|
|
if not getattr(app, "show_command_palette", False):
|
|
return
|
|
|
|
viewport = imgui.get_main_viewport()
|
|
center = viewport.get_center()
|
|
imgui.set_next_window_pos((center.x - 300, center.y - 200), imgui.Cond_.always)
|
|
imgui.set_next_window_size((600, 400), imgui.Cond_.always)
|
|
|
|
if not hasattr(app, "_command_palette_query"): app._command_palette_query = ""
|
|
if not hasattr(app, "_command_palette_selected"): app._command_palette_selected = 0
|
|
if not hasattr(app, "_command_palette_focused"): app._command_palette_focused = False
|
|
|
|
# Set focus on the window + input field ONCE per open.
|
|
if not app._command_palette_focused:
|
|
imgui.set_next_window_focus()
|
|
app._command_palette_focused = True
|
|
|
|
# Escape closes the palette.
|
|
if imgui.is_key_pressed(imgui.Key.escape):
|
|
_close_palette(app)
|
|
return
|
|
|
|
expanded, opened = imgui.begin("Command Palette##manual_slop", True, imgui.WindowFlags_.no_collapse)
|
|
if not expanded or not opened:
|
|
app.show_command_palette = False
|
|
app._command_palette_focused = False
|
|
imgui.end()
|
|
return
|
|
|
|
# After the window is drawn, the input gets focus.
|
|
if not getattr(app, '_command_palette_input_focused', False):
|
|
imgui.set_keyboard_focus_here()
|
|
app._command_palette_input_focused = True
|
|
|
|
# Process Up/Down/Enter BEFORE input_text so we see the keys before the
|
|
# input field consumes them for cursor movement / text editing.
|
|
results = fuzzy_match(app._command_palette_query, commands, top_n=20)
|
|
if results: app._command_palette_selected = max(0, min(app._command_palette_selected, len(results) - 1))
|
|
else: app._command_palette_selected = 0
|
|
|
|
if imgui.is_key_pressed(imgui.Key.down_arrow):
|
|
if results:
|
|
app._command_palette_selected = min(app._command_palette_selected + 1, len(results) - 1)
|
|
if imgui.is_key_pressed(imgui.Key.up_arrow):
|
|
if results:
|
|
app._command_palette_selected = max(app._command_palette_selected - 1, 0)
|
|
if imgui.is_key_pressed(imgui.Key.enter) or imgui.is_key_pressed(imgui.Key.keypad_enter):
|
|
if results and 0 <= app._command_palette_selected < len(results):
|
|
_execute(app, results[app._command_palette_selected].command)
|
|
|
|
imgui.set_next_item_width(-1)
|
|
_, app._command_palette_query = imgui.input_text("##query", app._command_palette_query)
|
|
|
|
if imgui.begin_child("##results", (0, -1)):
|
|
for i, scored in enumerate(results):
|
|
is_selected = (i == app._command_palette_selected)
|
|
label = f"[{scored.command.category}] {scored.command.title}"
|
|
clicked, _ = imgui.selectable(label, is_selected)
|
|
if clicked:
|
|
app._command_palette_selected = i
|
|
_execute(app, scored.command)
|
|
if not results:
|
|
imgui.text_disabled("No matching commands.")
|
|
imgui.end_child()
|
|
|
|
imgui.end()
|