"""
Claude Agent SDK applet for winston framework
Provides interactive Claude Code interface with long-running context
WITH STREAMING OUTPUT SUPPORT
"""
from typing import Any
import os
import sys
import re
import pygetwindow as gw  # type: ignore
import asyncio
import threading
import logging
logging.basicConfig(level=logging.WARNING)  # Only show warnings and errors from libraries
import queue
from collections import deque
import time
import json
from pathlib import Path
from datetime import datetime
from ..key_handler import KeyHandler
from ..elements import Element
from ..form_elements import ListElement, TextElement, ButtonElement
from ..subdialogs import Applet, FormDialog, InputPromptDialog
from .. import debugging
from ..framework import beep
from .. import punctuator
from ..format_mypy import format_mypy_output

try:
    from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
    from claude_agent_sdk import CLINotFoundError, ProcessError
    CLAUDE_SDK_AVAILABLE = True
except ImportError:
    CLAUDE_SDK_AVAILABLE = False


# Session history management functions
def get_session_history_path() -> Path:
    """Get path to session history JSON file."""
    data_dir = Path(__file__).parent.parent / 'data'
    data_dir.mkdir(exist_ok=True)
    return data_dir / 'claude_session_history.json'


def load_session_history() -> dict[str, Any]:
    """Load session history from JSON file."""
    history_path = get_session_history_path()
    if history_path.exists():
        try:
            with open(history_path, 'r') as f:
                return json.load(f)
        except Exception as e:
            print(f"Error loading session history: {e}")
            return {}
    return {}


def save_session_history(history: dict[str, Any]) -> None:
    """Save session history to JSON file."""
    history_path = get_session_history_path()
    try:
        with open(history_path, 'w') as f:
            json.dump(history, f, indent=2)
    except Exception as e:
        print(f"Error saving session history: {e}")


def add_session_to_history(session_id: str, description: str = "") -> bool:
    """Add a new session to history if not already present."""
    history = load_session_history()
    if session_id not in history:
        history[session_id] = {
            'timestamp': datetime.now().isoformat(),
            'description': description
        }
        save_session_history(history)
        return True
    return False


def update_session_description(session_id: str, description: str) -> bool:
    """Update the description for an existing session."""
    history = load_session_history()
    if session_id in history:
        # Handle old format (string timestamp) or new format (dict)
        if isinstance(history[session_id], str):
            history[session_id] = {
                'timestamp': history[session_id],
                'description': description
            }
        else:
            history[session_id]['description'] = description
        save_session_history(history)
        return True
    return False


class ClauderDialog(Applet, InputPromptDialog):  # type: ignore[misc]
    """Claude Code SDK applet with long-running session context and streaming.

    Provides:
    - Interactive Claude Code interface with streaming responses
    - Session persistence and history
    - Real-time tool usage and thinking announcements
    - Session management (new, continue, resume specific)
    """

    claude_thread: threading.Thread | None
    claude_client: Any  # ClaudeSDKClient
    claude_queue: queue.Queue[tuple[str, str | None]]
    response_queue: queue.Queue[tuple[str, Any]]
    session_active: bool
    session_status: str
    session_id: str
    output_buffer: deque[str]
    beep_enabled: bool
    session_mode: str | None
    query_start_time: float | None
    first_meaningful_prompt: str | None
    user_prompts: list[str]
    description_suggested: bool
    """
    Claude Code SDK applet with long-running session context
    """
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        if not CLAUDE_SDK_AVAILABLE:
            raise ImportError("claude-agent-sdk not available. Install with: pip install claude-agent-sdk")
        # Threading for async operations
        self.claude_thread = None
        self.claude_client = None
        self.claude_queue = queue.Queue()
        self.response_queue = queue.Queue()
        self.session_active = False
        self.session_status="Not started"
        self.session_id = 'not in session'
        self.session_stats='awaiting first result'
        self.session_lock = threading.Lock()
        self.beep_enabled = True  # Initial state is On
        self.query_start_time = None  # Track when current query started
        self.history: deque[str] = deque(maxlen=500)  # type: ignore[assignment]
        self.first_result_received = False  # Track if we've received first result
        self.user_prompt_count = 0  # Track number of user prompts (excluding "hello")
        self.first_meaningful_prompt = None  # Store first non-hello prompt
        self.description_suggestion_sent = False  # Track if we've already suggested a description
        self.session_mode = None  # Track what session mode user selected: "new", "continue", or session_id
        super().__init__(*args, **kwargs)
        # Session selection and startup moved to pre_run()

    def pre_run(self) -> None:
        """
        Called by Dialog.inner_run() after __init__ completes but before main loop starts.
        This is safe place to call input_values() for session selection.
        """
        # Show session selection dialog (now safe after __init__ complete)
        session_choice = self._show_session_selector()
        self.session_mode = session_choice  # Store for later reference

        # Claude Code configuration based on user choice
        # Set working directory to repository root (parent of winston package)
        repo_root = str(Path(__file__).parent.parent.parent)
        print(f'repo_root calculated as: {repo_root}')
        print(f'repo_root exists: {Path(repo_root).exists()}')
        print(f'repo_root is dir: {Path(repo_root).is_dir()}')

        if session_choice == "new":
            # Explicitly start fresh session with no continuation
            self.claude_options = ClaudeAgentOptions(
                allowed_tools=["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
                permission_mode="default",
                continue_conversation=False,
                resume=None,
                cwd=repo_root  # Set subprocess working directory
                # NOTE: No need for add_dirs or env - cwd should be sufficient
                # The subprocess will inherit this as its working directory
            )
        elif session_choice == "continue":
            # Continue most recent conversation
            self.claude_options = ClaudeAgentOptions(
                allowed_tools=["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
                permission_mode="default",
                continue_conversation=True,
                resume=None,
                cwd=repo_root  # Set subprocess working directory
            )
        else:
            # Resume specific session by ID
            self.claude_options = ClaudeAgentOptions(
                allowed_tools=["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
                permission_mode="default",
                continue_conversation=False,
                resume=session_choice,
                cwd=repo_root  # Set subprocess working directory
            )

        # Auto-start session
        self.start_claude_session()

        # Return None to continue with normal dialog startup
        return None
    @KeyHandler.hotkey("Control+E")
    def command_session(self, modified_name: str) -> None:
        id=self._show_session_selector()
        print('selected',id)

    def _on_edit_session(self, edit_button: Any) -> None:
        """Callback for Edit button to update session description"""
        dialog = edit_button.dialog
        session_list = dialog.element_from_name('Session')
        selection = dialog.Session

        # Only allow editing of actual session entries (not "New Session" or "Continue Most Recent")
        if selection in ["New Session", "Continue Most Recent"]:
            beep()
            return

        # Extract session_id from selection string
        try:
            session_id = selection.rsplit(' (')[1].strip(')')
        except:
            beep()
            return

        # Get current description
        history = load_session_history()
        session_data = history.get(session_id, {})
        if isinstance(session_data, str):
            # Old format - convert
            current_description = ""
        else:
            current_description = session_data.get('description', '')

        # Prompt for new description
        fields = [
            (TextElement, {'name': 'Description', 'value': current_description})
        ]
        result = self.input_values(title='Edit Session Description', fields=fields)

        if result and isinstance(result, dict):
            new_description = result['Description']
            update_session_description(session_id, new_description)

            # Rebuild session list with updated description
            history = load_session_history()
            session_options = ["New Session", "Continue Most Recent"]
            for sid, data in sorted(history.items(), key=lambda x: x[1]['timestamp'] if isinstance(x[1], dict) else x[1], reverse=True):
                if isinstance(data, str):
                    timestamp = data
                    description = ""
                else:
                    timestamp = data.get('timestamp', '')
                    description = data.get('description', '')

                try:
                    dt = datetime.fromisoformat(timestamp)
                    formatted = dt.strftime("%Y-%m-%d %H:%M")
                except:
                    formatted = timestamp

                # Format: "YYYY-MM-DD HH:MM description (session_id)"
                display = f"{formatted} {description} ({sid})"
                session_options.append(display)

            # Refill list and maintain selection on edited item
            updated_selection = f"{formatted} {new_description} ({session_id})"
            session_list.refill(session_options, updated_selection, None)
            dialog.set_from_name('Session')

    def _show_session_selector(self) -> str:
        """Show dialog to select session mode: new, continue, or resume specific session"""
        history = load_session_history()

        # Build session list with formatted dates and descriptions
        session_options = ["New Session", "Continue Most Recent"]
        for session_id, data in sorted(history.items(), key=lambda x: x[1]['timestamp'] if isinstance(x[1], dict) else x[1], reverse=True):
            # Handle both old format (string) and new format (dict)
            if isinstance(data, str):
                timestamp = data
                description = ""
            else:
                timestamp = data.get('timestamp', '')
                description = data.get('description', '')

            try:
                dt = datetime.fromisoformat(timestamp)
                formatted = dt.strftime("%Y-%m-%d %H:%M")
            except:
                formatted = timestamp

            # Format: "YYYY-MM-DD HH:MM description (session_id)"
            display = f"{formatted} {description} ({session_id})"
            session_options.append(display)

        # Use input_values() pattern with Edit button
        fields = [
            (ListElement, {'name': 'Session', 'values': session_options}),
            (ButtonElement, {'name': 'ok'}),
            (ButtonElement, {'name': 'cancel'}),
            (ButtonElement, {'name': 'Edit', 'on_press': self._on_edit_session})
        ]
        results = self.input_values(title='Select Session', fields=fields)

        if results and isinstance(results, dict) and results['__focus__']['name'].lower() != 'cancel':
            selection = results['Session']
            if selection == "New Session":
                return "new"
            elif selection == "Continue Most Recent":
                return "continue"
            else:
                # Extract session ID from formatted string
                session_id = selection.rsplit(' (')[1].strip(')')
                return session_id
        else:
            # Default to continue if cancelled
            return "continue"
    def content_elements(self, filler: Any, element_type: type[Element] = Element) -> list[Element]:
        """
        Return  initial content for the dialog
        """
        welcome_messages = [
            "Claude Agent SDK Interactive Session",
        ]
        contents = [element_type(self,'content',f) for f in welcome_messages]
        return contents
    def closing(self) -> None:
        """
        Clean shutdown of Claude session
        """
        self.stop_claude_session()
        super().closing()
    def say_current(self, interrupt: bool = True, suppress_indent_speech: bool = False, substitutions: dict[str, str] = punctuator.some) -> None:
        cleaned_line = str(self.current()).lstrip('-').replace('**','').replace('***','').replace('##','').replace('###','')
        self.say(cleaned_line,interrupt=interrupt,substitutions=substitutions)
    def start_claude_session(self) -> None:
        """
        Start the Claude Code session in a separate thread
        """
        if self.session_active:
            print('session already active when starting session')
            return
        try:
            self.claude_thread = threading.Thread(
                target=self._claude_worker,
                daemon=True,
                name="ClaudeCodeWorker"
            )
            self.session_active = True
            self.claude_thread.start()
            self.say_strings("Claude Code session starting...")
        except Exception as e:
            self.say_strings(f"Failed to start Claude session: {e}")
            self.session_active = False
            self.session_status='Session Starting'
    def stop_claude_session(self) -> None:
        """
        Stop the Claude Code session
        """
        with self.session_lock:
            if self.session_active:
                self.session_active = False
                self.session_status='Stopping'
                self.claude_queue.put(("QUIT", None))
                if self.claude_thread and self.claude_thread.is_alive():
                    self.claude_thread.join(timeout=2.0)
    def ok(self, modified_name: str) -> None:
        self.say_strings("stopping claude code")
        self.stop_claude_session()
        super().ok(modified_name)
    @KeyHandler.hotkey("Control+H")
    def command_inspect_history(self, modified_name: str) -> None:
        import sys
        frame = sys._getframe()  # Get current frame
        self.context = debugging.Context('search results',self.home_dialog,frame,None)  # type: ignore[arg-type]
        debugging.inspect_object(dialog=self,object=self.history,context=self.context,name='message history')      
    @KeyHandler.hotkey("Prior")
    def command_prior_locution(self, modified_name: str) -> None:
        lineno=max(self.cursor-1,0)
        while lineno>0:  
            if  not self[lineno].value.startswith(('Claude said:','User said:')):
                lineno-=1
            else:
                break
        self.move(lineno,0,shift=False)
    @KeyHandler.hotkey("Next")
    def command_next_locution(self, modified_name: str) -> None:
        lineno=min(self.cursor+1,len(self)-2)
        while lineno<len(self)-2:  
            if  not self[lineno].value.startswith(('Claude said:','User said:')):
                lineno+=1
            else:
                break
        self.move(lineno,0,shift=False)            
    def _claude_worker(self) -> None:
        """
        Worker thread for Claude Code operations
        Runs asyncio event loop separate from main pythoncom loop
        """
        try:
            # Create new event loop for this thread
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(self._claude_async_worker())
        except Exception as e:
            self.response_queue.put(("ERROR", f"Claude worker error: {e}"))
        finally:
            try:
                loop.close()
            except:
                pass
    async def _claude_async_worker(self) -> None:
        """
        Async worker that manages Claude Code client
        """
        try:
            # Initialize Claude client
            self.claude_client = ClaudeSDKClient(options=self.claude_options)
            self.say_strings('starting claude session')
            await self.claude_client.__aenter__()
            self.response_queue.put(("STATUS", "Claude Code session ready"))
            self.server_info = await self.claude_client.get_server_info()
            # Queue startup message that will trigger hello to Claude
            self.response_queue.put(("STARTUP_READY", None))
            # Main processing loop
            while self.session_active:
                try:
                    # Check for new prompts (non-blocking)
                    try:
                        command, prompt = self.claude_queue.get_nowait()
                        if command == "QUIT":
                            break
                        elif command == "PROMPT" and isinstance(prompt, str):
                            await self._handle_prompt(prompt)
                    except queue.Empty:
                        # No new prompts, sleep briefly
                        await asyncio.sleep(0.1)
                        continue
                except Exception as e:
                    self.response_queue.put(("ERROR", f"Error processing prompt: {e}"))
                    continue  # Keep the worker loop running after errors
        except CLINotFoundError:
            self.response_queue.put(("ERROR", "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code"))
        except Exception as e:
            self.response_queue.put(("ERROR", f"Failed to initialize Claude: {e}"))
        finally:
            # Clean shutdown
            if self.claude_client:
                try:
                    await self.claude_client.__aexit__(None, None, None)
                except:
                    pass
    def _message_type(self, message: Any) -> str:
        return 'result' if (type(message).__name__ ==  "ResultMessage") else getattr(message,'type',type(message).__name__)
    async def _handle_prompt(self, prompt: str) -> None:
        """
        REFACTORED: Send prompt to Claude and queue responses immediately
        This method now only handles sending the prompt and queuing all responses/blocks
        """
        try:
            # Send query
            await self.claude_client.query(prompt)
            # Stream response as it arrives - queue everything immediately
            async for message in self.claude_client.receive_response():
                self.response_queue.put((self._message_type(message),message))  
        except Exception as e:
            self.response_queue.put(("ERROR", f"Error handling prompt: {e}"))
    def ingest_input(self, prompt: str) -> None:
        """
        Send a prompt to Claude (called from main thread)
        put prompt into conversation log
        """
        if not self.session_active:
            self.start_claude_session()
            # Give it a moment to initialize
            time.sleep(0.5)
        if self.session_active:
            # Track user prompts and trigger description suggestion
            # Filter out hello messages and session mode greetings
            prompt_lower = prompt.strip().lower()
            is_hello = prompt_lower in ["hello", "hi"] or prompt_lower.startswith("hello (this is a ")
            if not is_hello:
                self.user_prompt_count += 1
                if self.user_prompt_count == 1:
                    self.first_meaningful_prompt = prompt
                    # Check if current session has no description
                    self._maybe_suggest_description()

            self.claude_queue.put(("PROMPT",prompt))
            self.history.append(prompt)
            self.session_status="Waiting for Claude"
            self.query_start_time = time.time()  # Start timing the query
            self._add_conversation_element("Prompt", prompt)
        else:
            self.say_strings("Claude session not available")
    def _maybe_suggest_description(self) -> None:
        """
        Check if session needs a description and suggest one based on first meaningful prompt.
        Only triggers once per session, after first non-hello user prompt.
        """
        if self.description_suggestion_sent or not self.first_meaningful_prompt:
            return

        # Check if current session has no description
        history = load_session_history()
        session_data = history.get(self.session_id, {})

        # Handle both old format (string) and new format (dict)
        has_description = False
        if isinstance(session_data, dict):
            description = session_data.get('description', '').strip()
            has_description = bool(description)

        # If session is unnamed, inject a suggestion prompt to Claude
        if not has_description:
            self.description_suggestion_sent = True
            suggestion_prompt = (
                f"Based on the user's request: '{self.first_meaningful_prompt}', "
                "suggest a concise 3-5 word description for this session. "
                "Start your response with 'Session description:' followed by your suggested description. "
                "Then proceed to answer the user's request."
            )
            # Queue the suggestion request before the actual user prompt gets processed
            # This will be read by Claude as a system-level instruction
            self.claude_queue.put(("PROMPT", suggestion_prompt))
            self.history.append(f"[Auto-description request: {self.first_meaningful_prompt[:50]}...]")

    def _extract_and_save_description(self, text: str) -> None:
        """
        Extract session description from Claude's response if present.
        Looks for pattern: "Session description: description text"
        Automatically saves the description to session history.
        """
        if not text or not isinstance(text, str):
            return

        # Look for "Session description:" followed by the description text
        match = re.search(r'Session description:\s*(.+?)(?:\n|$)', text, re.IGNORECASE)
        if match:
            description = match.group(1).strip()
            # Remove surrounding quotes if present
            description = description.strip('"\'')
            # Remove trailing punctuation
            description = description.rstrip('.,;:!?')

            # Skip if it looks like a placeholder (contains angle brackets)
            if '<' in description or '>' in description:
                return

            # Save to session history
            if self.session_id and description:
                if update_session_description(self.session_id, description):
                    # Optionally log that we saved it (for debugging)
                    self.history.append(f"[Auto-saved description: {description}]")

    def get_input(self) -> str | bool | Any:
        """
        Override get_input to process Claude responses during waits
        """
        assert self.app is not None, "app must be set"
        # Process any pending Claude responses first
        tick_count=0
        self._process_claude_responses()
        if self.dismiss:
            return False
        # Use timeout loop to allow Claude response processing during waits
        while True:
            try:
                inp = self.key_q.get(timeout=0.1)  # 100ms timeout
                break  # Got input, exit the timeout loop
            except queue.Empty:
                # Check for quit condition during timeout
                if self.app.quit_now:
                    return False
                # Process Claude responses during timeout
                #print('in clauder get_input, polling claude response queue')
                self._process_claude_responses()
                tick_count+=1
                if tick_count==50: #5 seconds
                    # Only beep if we're waiting for Claude to respond, not for user input
                    if self.session_status == "Waiting for Claude" and self.beep_enabled:
                        beep(200,250)
                    tick_count=0
                continue  # Continue waiting for input
        # Now handle the actual input we received
        if self.app.quit_now or (inp == 'quit_now'):
            # Handle quit conditions
            if hasattr(self.app, 'debugger') and self.app.debugger.applet is self:
                self.app.debugger.set_quit()
                raise self.app.WinQuit()  # type: ignore[attr-defined]
            else:
                self.unwind(stop_dialog=None)
                result = False
        elif hasattr(self.app, 'debugging') and isinstance(inp, self.app.debugging.AdultContent):
            # Handle debugger content
            self.home_dialog.consume_adult_content(inp)  # type: ignore[attr-defined]
            result = self.unwind(stop_dialog=self.home_dialog)  # type: ignore[assignment]
        else:
            # Normal input - pass it on
            result = inp
        return result
    def save_title_and_hwnd(self) -> None:
        assert self.app is not None, "app must be set"
        aw = gw.getActiveWindow()
        #allow up to 3 seconds for claude to grab the title and handle
        t = 0.0  # Use float to match increment
        while t < 3:
            #print(aw._hWnd, self.app.window_handles, aw._hWnd in self.app.window_handles)
            if (aw._hWnd not in self.app.window_handles) or (aw.title not in self.app.window_titles):
                self.app.window_handles.append(aw._hWnd)
                self.app.window_titles.append(aw.title)
                #print('breaking after appending to handles and titles:', self.app.window_handles, self.app.window_titles)
                break
            time.sleep(0.3)
            t = t + 0.3                    
    def _metadata(self, message: Any) -> str:
        cost = getattr(message, 'total_cost_usd', 0)
        duration = getattr(message, 'duration_ms', 0)
        turns = getattr(message, 'num_turns', 0)    
        return  f"Cost: ${cost:.4f} | Duration: {duration/1000:.2f}seconds | Turns: {turns}"    
    def _process_claude_responses(self) -> None:
        while True:
            try:
                response_type, message= self.response_queue.get_nowait()
                self.history.append(message)
                # Display response type and message content in output area
                if response_type == "STATUS":
                    self.say_strings(message)
                    if self.session_status=='Starting':
                        self.save_title_and_hwnd()
                    self.session_status=message
                elif response_type == "STARTUP_READY":
                    # Session is ready, send hello message to Claude with session mode info
                    if self.session_mode == "new":
                        greeting = "hello (this is a NEW session, not a continuation)"
                    elif self.session_mode == "continue":
                        greeting = "hello (this is a CONTINUATION of the most recent session)"
                    else:
                        greeting = f"hello (this is a RESUMED session: {self.session_mode})"
                    self.ingest_input(greeting)
                elif response_type == "result":
                    # indicates this was a ResultMessage which signifies end of the response sequence so reset response tracking variables and  store  completion metadata
                    if self.beep_enabled:
                        beep(300,1000)
                    self.session_status="Awaiting input"
                    self.query_start_time = None  # Clear query timer
                    self.session_stats=self._metadata(message)
                    self.session_id = getattr(message, 'session_id', 'unidentifiable  active session')
                    # Add session to history on first result
                    if not self.first_result_received:
                        self.first_result_received = True
                        add_session_to_history(self.session_id)                    
                else:
                    #all responses except for status changes and the final result message that indicates end of a query response
                    txt = message if isinstance(message,str) else ""
                    self._add_conversation_element(response_type,txt,message)
                    content = getattr(message,'content',[])
                    for block in content:
                        block_type = type(block).__name__
                        txt = block if isinstance(block,str) else getattr(block,'text', "")

                        # Check for session description suggestion in response
                        self._extract_and_save_description(txt)

                        for index,line in enumerate(txt.split('\n')):
                            line_type=block_type if not index else f'line {index}'
                            prefix = response_type if not index else block_type
                            self._add_conversation_element(f"{prefix}.{line_type}", line,message)
            except queue.Empty:
                break
    def _add_conversation_element(self, content_type: str, content: str, message: Any = None) -> None:
        """
        Add a prompt or response element to the dialog
        omit blank lines whatever the message type as not useful to user - will still be in history 
        mark start of new locutor with 'Claude/User said: '
        element name is the message type or qualified block type or continuation line number
        """
        output_start=max(len(self)-1,0)
        if not len(content.strip()):
            return 
        try:
            #prepend 'claude said:' or 'User said:' to first line of actual claude output in a response 
            if message and ((len(self)<2) or (self[-2].name=='Prompt')):
                content='Claude said: '+content 
            elif ((content_type=='Prompt') and ((len(self)<2) or (self[-2].name!='Prompt'))): 
                content='User said: '+content                 
            self.insert(len(self)-1, Element(self, content_type, content))
            self[-2].message = message
            # Move to show the new content
            self.move(output_start, 0, False, silent=False)
        except Exception as e:
            print(f"Error adding response element: {e}")
    def zzz_start_streaming_element(self, content_type: str, initial_content: str) -> Element | None:
        """
        Start a new streaming response element
        """
        try:
            output_start = max(len(self)-2, 0)
            element = Element(self, content_type, initial_content)
            self.insert(len(self)-1, element)
            # Move to show the new content
            self.move(output_start, 0, False, silent=True)
            return element
        except Exception as e:
            print(f"Error starting streaming element: {e}")
            return None
    def zzz_append_to_streaming_element(self, element: Element | None, content: str) -> None:
        """
        Append content to the current streaming element
        """
        try:
            if element and hasattr(element, 'value'):
                # For streaming content, we'll handle line breaks by creating new elements
                lines = content.split('\n')
                # First line gets appended to current element
                if lines[0]:
                    element.value += lines[0]
                # Additional lines create new elements
                for line in lines[1:]:
                    if line.strip() or len(lines) > 1:  # Include empty lines if there are multiple lines
                        new_element = Element(self, "Claude", line)
                        self.insert(len(self)-1, new_element)
                        element = new_element  # Update reference for potential further appending
                # Keep the view updated
                self.move(len(self)-2, 0, False, silent=True)
        except Exception as e:
            print(f"Error appending to streaming element: {e}")
    @KeyHandler.hotkey("Control+R")
    def command_restart_session(self, modified_name: str) -> None:
        """
        Restart the Claude Code session
        """
        self.stop_claude_session()
        time.sleep(0.5)  # Brief pause
        self.start_claude_session()
        self.say_strings("Restarting Claude Code session...")
    @KeyHandler.hotkey("Control+S")
    def command_session_status(self, modified_name: str) -> None:
        """
        Report session status
        """
        activation_state= "active " if self.session_active else "inactive"
        thread_status = "running" if (self.claude_thread and self.claude_thread.is_alive()) else "stopped"
        # Add timer information if we're waiting for Claude
        timer_info = ""
        if self.session_status == "Waiting for Claude" and self.query_start_time:
            elapsed = int(time.time() - self.query_start_time)
            timer_info = f" ({elapsed}s elapsed)"
        self.say_strings([f"Claude session: {activation_state}, {self.session_status}{timer_info}, worker thread: {thread_status}. ",'session stats: ',self.session_stats])
    @KeyHandler.hotkey("Control+B")
    def command_toggle_beep(self, modified_name: str) -> None:
        """
        Toggle beep notifications when Claude is working
        """
        self.beep_enabled = not self.beep_enabled
        state = "On" if self.beep_enabled else "Off"
        self.say_strings(f"Beep notifications: {state}")
    @KeyHandler.hotkey("Control+C")
    def command_clear_history(self, modified_name: str) -> None:
        """
        Clear the conversation history but keep session alive
        """
        if self.message(title="Clear conversation history?", buttons=["Yes", "No"]) == "Yes":
            # Remove all elements except welcome messages and input
            elements_to_remove = []
            for i, element in enumerate(self):
                if i >= 5 and i < len(self) - 1:  # Keep first 5 (welcome) and last (input)
                    elements_to_remove.append(i)
            # Remove in reverse order to maintain indices
            for i in reversed(elements_to_remove):
                del self[i]
            self.say_strings("Conversation history cleared")
            self.move(len(self)-1, 0, False)  # Move to input line
        else:
            self.say_strings("Cancelled")
    @KeyHandler.hotkey("Control+M")
    def command_run_mypy(self, modified_name: str) -> None:
        """
        Run mypy type checker, reformat output to be blind-friendly, and save to scratch/
        """
        import subprocess
        import re
        from pathlib import Path

        self.say_strings("Running mypy type checker...")

        try:
            # Get winston directory (parent of applets/) and parent directory
            winston_dir = Path(__file__).parent.parent
            parent_dir = winston_dir.parent

            # Run mypy from parent directory on winston package
            # This allows relative imports to work correctly
            result = subprocess.run(
                ['python', '-m', 'mypy', 'winston', f'--config-file=winston{os.sep}mypy.ini'],
                cwd=parent_dir,
                capture_output=True,
                text=True,
                timeout=60
            )

            # Combine stdout and stderr
            output_lines = (result.stdout + result.stderr).splitlines()

            # Format output using format_mypy module
            formatted_lines, summary_info = format_mypy_output(output_lines)

            # Save to scratch/mypy_errors_complete.txt
            scratch_dir = winston_dir / 'scratch'
            scratch_dir.mkdir(exist_ok=True)
            output_file = scratch_dir / 'mypy_errors_complete.txt'

            with open(output_file, 'w', encoding='utf-8') as f:
                f.write('\n'.join(formatted_lines))

            # Speak the summary from mypy
            if summary_info:
                self.say_strings(summary_info)
            else:
                # Fallback if no summary (shouldn't happen with normal mypy output)
                error_count = len(formatted_lines)
                if error_count == 0:
                    self.say_strings("No mypy errors found.")
                else:
                    self.say_strings(f"Found {error_count} errors.")

        except subprocess.TimeoutExpired:
            self.say_strings("Mypy command timed out after 60 seconds")
        except FileNotFoundError:
            self.say_strings("Error: mypy not found. Install with: pip install mypy")
        except Exception as e:
            self.say_strings(f"Error running mypy: {e}")

    @KeyHandler.hotkey("F1")
    def command_help(self, modified_name: str) -> None:
        """
        Show help information
        """
        help_text = [
            "",
            "Claude Code Applet Help - STREAMING VERSION:",
            "• Type prompts and press Enter to interact with Claude",
            "• Claude can read files, run commands, and write code",
            "• Responses now stream in real-time as Claude types",
            "• Tool usage and thinking are announced immediately",
            "• Control+R: Restart Claude session",
            "• Control+S: Show session status",
            "• Control+C: Clear conversation history",
            "• Control+B: Toggle beep notifications",
            "• Control+M: Run mypy and save results to scratch/",
            "• F1: Show this help",
            "• Alt+Left: Close applet",
            ""
        ]
        for text in help_text:
            self.insert(len(self)-1, Element(self, 'help', text))
        self.move(len(self)-len(help_text)-1, 0, False)
        self.say_strings("Help displayed")