from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from pydantic import BaseModel

import uvicorn
import webbrowser
import threading
import time
from pathlib import Path
from .config import get_global_config
from .ai.chat import generate_chat_reply, clean_ai_reply

app = FastAPI(title="kage UI")

INDEX_HTML = """
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>kage Dashboard</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <style>
        :root {
            /* Dark Theme (Default) */
            --bg-base: #0d1117;
            --bg-sidebar: #161b22;
            --bg-card: #161b22;
            --bg-card-hover: #1c2128;
            --bg-input: #010409;
            --bg-terminal: #010409;
            
            --text-primary: #c9d1d9;
            --text-secondary: #8b949e;
            --text-dim: #484f58;
            
            --border-base: #30363d;
            --border-hover: #444c56;
            
            --accent: #58a6ff;
            --accent-muted: rgba(88, 166, 255, 0.1);
            
            --success: #238636;
            --success-text: #3fb950;
            --error: #da3633;
            --error-text: #f85149;
            --warning: #9e6a03;
            --warning-text: #d29922;
            
            --sidebar-width: 260px;
            --sidebar-collapsed-width: 68px;
            --header-height: 64px;
            --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        }

        @media (prefers-color-scheme: light) {
            :root {
                --bg-base: #f6f8fa;
                --bg-sidebar: #ffffff;
                --bg-card: #ffffff;
                --bg-card-hover: #fcfcfc;
                --bg-input: #ffffff;
                --bg-terminal: #0d1117; /* Keep terminal dark-ish */
                
                --text-primary: #1f2328;
                --text-secondary: #636c76;
                --text-dim: #8c959f;
                
                --border-base: #d0d7de;
                --border-hover: #afb8c1;
                
                --accent: #0969da;
                --accent-muted: rgba(9, 105, 218, 0.1);
                
                --success: #1a7f37;
                --success-text: #1a7f37;
                --error: #cf222e;
                --error-text: #cf222e;
                --warning: #9a6700;
                --warning-text: #9a6700;
            }
        }

        * {
            box-sizing: border-box;
            -webkit-font-smoothing: antialiased;
        }

        body {
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
            background-color: var(--bg-base);
            color: var(--text-primary);
            margin: 0;
            padding: 0;
            display: flex;
            height: 100vh;
            overflow: hidden;
        }

        /* Sidebar */
        .sidebar {
            width: var(--sidebar-width);
            background-color: var(--bg-sidebar);
            border-right: 1px solid var(--border-base);
            display: flex;
            flex-direction: column;
            z-index: 100;
            transition: width var(--transition);
            position: relative;
        }

        .sidebar.collapsed {
            width: var(--sidebar-collapsed-width);
        }

        .sidebar-header {
            padding: 16px 24px;
            height: var(--header-height);
            display: flex;
            align-items: center;
            justify-content: space-between;
            border-bottom: 1px solid var(--border-base);
        }

        .sidebar.collapsed .sidebar-header {
            padding: 16px 0;
            justify-content: center;
        }

        #sidebar-toggle {
            background: none;
            border: none;
            color: var(--text-secondary);
            cursor: pointer;
            display: flex;
            align-items: center;
            padding: 6px;
            border-radius: 6px;
            transition: all var(--transition);
        }

        #sidebar-toggle:hover {
            background-color: var(--accent-muted);
            color: var(--accent);
        }

        .sidebar.collapsed #sidebar-toggle {
            transform: rotate(0deg);
        }

        .logo {
            font-size: 1.25rem;
            font-weight: 700;
            color: var(--accent);
            letter-spacing: -0.5px;
            text-decoration: none;
            white-space: nowrap;
            overflow: hidden;
            transition: opacity var(--transition);
        }

        .sidebar.collapsed .logo-text {
            display: none;
        }

        .nav-list {
            padding: 16px 12px;
            flex-grow: 1;
            list-style: none;
            margin: 0;
        }

        .nav-item {
            padding: 10px 12px;
            border-radius: 6px;
            color: var(--text-secondary);
            text-decoration: none;
            display: flex;
            align-items: center;
            gap: 10px;
            font-weight: 500;
            margin-bottom: 4px;
            cursor: pointer;
            transition: all var(--transition);
            white-space: nowrap;
            position: relative;
        }

        .sidebar.collapsed .nav-item {
            justify-content: center;
            padding: 10px 0;
            gap: 0;
        }

        .nav-text {
            transition: opacity var(--transition);
        }

        .sidebar.collapsed .nav-text {
            display: none;
        }

        /* Hover Label for Collapsed Sidebar */
        .nav-item::after {
            content: attr(data-label);
            position: absolute;
            left: calc(100% + 12px);
            background-color: var(--bg-sidebar);
            color: var(--text-primary);
            padding: 6px 12px;
            border-radius: 4px;
            font-size: 0.8rem;
            border: 1px solid var(--border-base);
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.1s ease;
            z-index: 1000;
        }

        .sidebar.collapsed .nav-item:hover::after {
            opacity: 1;
        }

        .nav-item:hover {
            background-color: var(--accent-muted);
            color: var(--accent);
        }

        .nav-item.active {
            background-color: var(--accent);
            color: white;
        }

        .sidebar-footer {
            padding: 16px;
            border-top: 1px solid var(--border-base);
            font-size: 0.8rem;
            color: var(--text-dim);
            white-space: nowrap;
            overflow: hidden;
        }

        .sidebar.collapsed .sidebar-footer {
            display: none;
        }

        /* Main Content */
        .main {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            height: 100vh;
            overflow: hidden;
            position: relative;
        }

        .header {
            height: var(--header-height);
            padding: 0 24px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background-color: var(--bg-base);
            border-bottom: 1px solid var(--border-base);
            flex-shrink: 0;
        }

        .header-title {
            font-size: 1rem;
            font-weight: 600;
        }

        .header-actions {
            display: flex;
            align-items: center;
            gap: 16px;
        }

        .github-link {
            display: flex;
            align-items: center;
            color: var(--text-secondary);
            text-decoration: none;
            transition: color var(--transition);
        }

        .github-link:hover {
            color: var(--accent);
        }

        .github-link svg {
            width: 20px;
            height: 20px;
            fill: currentColor;
        }

        .content-area {
            flex-grow: 1;
            overflow-y: auto;
            padding: 24px;
            scrollbar-width: thin;
        }

        .section {
            display: none;
            max-width: 1200px;
            margin: 0 auto;
        }

        .section.active {
            display: block;
        }

        /* Log Features */
        .toolbar {
            display: flex;
            gap: 12px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        .search-container {
            flex-grow: 1;
            position: relative;
        }

        .search-input {
            width: 100%;
            background-color: var(--bg-input);
            border: 1px solid var(--border-base);
            border-radius: 6px;
            padding: 8px 12px;
            color: var(--text-primary);
            font-family: inherit;
        }

        .search-input:focus {
            outline: none;
            border-color: var(--accent);
            box-shadow: 0 0 0 3px var(--accent-muted);
        }

        .filter-select {
            background-color: var(--bg-input);
            border: 1px solid var(--border-base);
            border-radius: 6px;
            padding: 8px 12px;
            color: var(--text-primary);
            cursor: pointer;
        }

        .log-card {
            background-color: var(--bg-card);
            border: 1px solid var(--border-base);
            border-radius: 8px;
            margin-bottom: 16px;
            overflow: hidden;
            transition: border-color var(--transition);
        }

        .log-card:hover {
            border-color: var(--border-hover);
        }

        .log-header {
            padding: 12px 16px;
            background-color: rgba(0,0,0,0.02);
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: pointer;
            user-select: none;
        }

        .log-info {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .log-name {
            font-weight: 600;
        }

        .status-badge {
            padding: 2px 8px;
            border-radius: 12px;
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
        }

        .status-badge.SUCCESS { background: rgba(35, 134, 54, 0.15); color: var(--success-text); }
        .status-badge.FAILED, .status-badge.ERROR { background: rgba(218, 54, 51, 0.15); color: var(--error-text); }
        .status-badge.RUNNING { background: rgba(184, 115, 51, 0.15); color: var(--warning-text); }
        .status-badge.STOPPED { background: rgba(110, 118, 129, 0.15); color: var(--text-dim); }
        .status-badge.TIMEOUT { background: rgba(218, 54, 51, 0.1); color: var(--text-dim); }

        .log-meta {
            display: flex;
            align-items: center;
            gap: 16px;
            font-size: 0.85rem;
            color: var(--text-secondary);
        }

        .log-body {
            display: none;
            padding: 0;
            border-top: 1px solid var(--border-base);
        }

        .log-card.expanded .log-body {
            display: block;
        }

        .terminal-header {
            padding: 8px 16px;
            background-color: var(--bg-sidebar);
            font-size: 0.75rem;
            color: var(--text-dim);
            display: flex;
            justify-content: space-between;
        }

        .terminal {
            background-color: var(--bg-terminal);
            color: #d1d5db;
            padding: 16px;
            font-family: 'JetBrains Mono', monospace;
            font-size: 0.85rem;
            line-height: 1.5;
            overflow-x: auto;
            max-height: 500px;
            position: relative;
        }

        .line-numbers {
            display: inline-block;
            color: var(--text-dim);
            text-align: right;
            padding-right: 12px;
            margin-right: 12px;
            border-right: 1px solid var(--text-dim);
            user-select: none;
        }

        /* Config Cards */
        .config-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
            gap: 20px;
        }

        .card {
            background-color: var(--bg-card);
            border: 1px solid var(--border-base);
            border-radius: 8px;
            padding: 20px;
        }

        .card-title {
            font-size: 1.1rem;
            font-weight: 600;
            margin: 0 0 16px 0;
            color: var(--accent);
        }

        .kv-row {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid var(--border-base);
        }

        .kv-row:last-child { border-bottom: none; }
        .kv-key { color: var(--text-secondary); font-size: 0.9rem; }
        .kv-val { font-weight: 500; font-size: 0.9rem; }

        /* Buttons & Controls */
        .btn {
            background-color: var(--accent);
            color: white;
            border: none;
            border-radius: 6px;
            padding: 8px 16px;
            font-weight: 600;
            cursor: pointer;
            transition: opacity 0.2s;
        }

        .btn:hover { opacity: 0.9; }
        .btn:disabled { background-color: var(--border-base); cursor: not-allowed; }
        .btn-stop {
            background: none;
            border: 1px solid var(--error);
            color: var(--error-text);
            padding: 2px 8px;
            font-size: 0.7rem;
            border-radius: 4px;
            margin-left: 8px;
            transition: all 0.2s;
        }
        .btn-stop:hover {
            background: var(--error);
            color: white;
        }

        /* Animation */
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(4px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .task-card, .log-card {
            animation: fadeIn 0.3s ease-out forwards;
        }

        /* Toggle Switch */
        .switch {
            position: relative;
            display: inline-block;
            width: 36px;
            height: 20px;
        }

        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: var(--border-base);
            transition: .4s;
            border-radius: 20px;
        }

        .slider:before {
            position: absolute;
            content: "";
            height: 14px;
            width: 14px;
            left: 3px;
            bottom: 3px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        
        .switch input:checked + .slider { background-color: var(--accent); }
        .switch input:checked + .slider:before { transform: translateX(16px); }

        /* Task Details Expansion */
        .task-details-toggle {
            width: 100%;
            padding: 8px;
            margin-top: 12px;
            background: none;
            border: 1px dashed var(--border-base);
            border-radius: 6px;
            color: var(--text-secondary);
            font-size: 0.8rem;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            transition: background var(--transition);
        }

        .task-details-toggle:hover {
            background-color: var(--accent-muted);
            border-color: var(--accent);
            color: var(--accent);
        }

        .task-details-content {
            display: none;
            margin-top: 12px;
            padding-top: 12px;
            border-top: 1px solid var(--border-base);
            animation: fadeIn 0.2s ease-out;
        }

        .card.expanded .task-details-content {
            display: block;
        }

        .card.expanded .details-icon {
            transform: rotate(180deg);
        }

        .details-icon {
            transition: transform var(--transition);
        }

        .prompt-container {
            margin-top: 12px;
        }

        .prompt-label {
            font-size: 0.75rem;
            color: var(--text-dim);
            margin-bottom: 4px;
            display: block;
            font-weight: 600;
            text-transform: uppercase;
        }

        .prompt-code {
            background-color: var(--bg-terminal);
            color: #d1d5db;
            padding: 12px;
            border-radius: 6px;
            font-family: 'JetBrains Mono', monospace;
            font-size: 0.85rem;
            white-space: pre-wrap;
            word-break: break-all;
            border: 1px solid var(--border-base);
            max-height: 300px;
            overflow-y: auto;
        }

        .card.inactive {
            opacity: 0.6;
            filter: grayscale(0.5);
        }

        .card-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 12px;
        }

        /* Chat Styles */
        .chat-container {
            height: 60vh;
            background: var(--bg-card);
            border: 1px solid var(--border-base);
            border-radius: 8px;
            padding: 20px;
            overflow-y: auto;
            margin-bottom: 20px;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .chat-provider-badge {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            padding: 4px 12px;
            background: var(--accent-muted);
            border: 1px solid var(--accent);
            border-radius: 20px;
            font-size: 0.75rem;
            color: var(--accent);
            font-weight: 600;
            margin-bottom: 8px;
        }

        .chat-input-area {
            display: flex;
            gap: 12px;
        }

        .chat-input-area textarea {
            flex-grow: 1;
            background: var(--bg-input);
            border: 1px solid var(--border-base);
            border-radius: 8px;
            padding: 12px;
            color: var(--text-primary);
            font-family: inherit;
            resize: none;
            height: 60px;
        }

        .chat-input-area textarea:focus {
            outline: none;
            border-color: var(--accent);
            box-shadow: 0 0 0 3px var(--accent-muted);
        }

        .message {
            padding: 12px;
            border-radius: 8px;
            max-width: 80%;
            word-wrap: break-word;
            overflow-wrap: break-word;
            white-space: pre-wrap;
        }

        .message.user {
            align-self: flex-end;
            background: var(--accent);
            color: white;
        }

        .message.assistant {
            background: var(--bg-sidebar);
            border: 1px solid var(--border-base);
        }

        .message.assistant pre {
            margin: 0;
            white-space: pre-wrap;
            word-break: break-word;
            overflow-wrap: break-word;
            font-family: 'JetBrains Mono', monospace;
            font-size: 0.85rem;
        }

        /* Typing Indicator */
        .typing-indicator {
            display: flex;
            align-items: center;
            gap: 4px;
            padding: 12px 16px;
            background: var(--bg-sidebar);
            border: 1px solid var(--border-base);
            border-radius: 8px;
            max-width: 80px;
        }

        .typing-indicator span {
            display: inline-block;
            width: 8px;
            height: 8px;
            background: var(--text-dim);
            border-radius: 50%;
            animation: typingBounce 1.4s infinite ease-in-out;
        }

        .typing-indicator span:nth-child(1) { animation-delay: 0s; }
        .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
        .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }

        @keyframes typingBounce {
            0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
            40% { transform: scale(1); opacity: 1; }
        }
        /* Modal */
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            align-items: center;
            justify-content: center;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .modal.active {
            display: flex;
            opacity: 1;
        }
        .modal-content {
            background-color: var(--bg-card);
            border: 1px solid var(--border-base);
            border-radius: 12px;
            width: 90%;
            max-width: 600px;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            box-shadow: 0 8px 32px rgba(0,0,0,0.4);
            animation: modalIn 0.3s ease-out;
        }
        @keyframes modalIn {
            from { transform: scale(0.95); opacity: 0; }
            to { transform: scale(1); opacity: 1; }
        }
        .modal-header {
            padding: 16px 20px;
            border-bottom: 1px solid var(--border-base);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .modal-body {
            padding: 20px;
            line-height: 1.6;
        }
        .close-modal {
            background: none;
            border: none;
            color: var(--text-secondary);
            cursor: pointer;
            font-size: 1.5rem;
            display: flex;
            align-items: center;
            justify-content: center;
        }
    </style>
</head>
<body>
    <aside class="sidebar" id="sidebar">
        <div class="sidebar-header">
            <a href="/" class="logo">🌑<span class="logo-text" style="margin-left:8px">kage</span></a>
            <button id="sidebar-toggle" title="Toggle Sidebar">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
            </button>
        </div>
        <nav class="nav-list">
            <div class="nav-item active" data-section="runs" id="nav-runs" data-label="Runs">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
                <span class="nav-text">Runs</span>
            </div>
            <div class="nav-item" data-section="config" id="nav-config" data-label="Settings & Tasks">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
                <span class="nav-text">Settings & Tasks</span>
            </div>
            <div class="nav-item" data-section="chat" id="nav-chat" data-label="AI Chat">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.1a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
                <span class="nav-text">AI Chat</span>
            </div>
            <div class="nav-item" data-section="connectors" id="nav-connectors" data-label="Connectors">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 2v4"/><path d="M16 2v4"/><rect x="4" y="8" width="16" height="12" rx="2"/></svg>
                <span class="nav-text">Connectors</span>
            </div>
        </nav>
        <div class="sidebar-footer">
            v0.3.5 - Autonomous Exec layer
        </div>
    </aside>

    <main class="main">
        <header class="header">
            <div class="header-title" id="page-title">Runs</div>
            <div class="header-actions">
                <a class="github-link" href="https://github.com/igtm/kage" target="_blank" title="GitHub">
                    <svg viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.67 0 8.2c0 3.63 2.29 6.71 5.47 7.8.4.08.55-.18.55-.39 0-.19-.01-.83-.01-1.5-2.01.38-2.53-.5-2.69-.96-.09-.23-.48-.96-.82-1.16-.28-.15-.68-.54-.01-.55.63-.01 1.08.59 1.23.84.72 1.24 1.87.89 2.33.68.07-.54.28-.89.5-1.1-1.78-.21-3.64-.92-3.64-4.08 0-.9.31-1.64.82-2.22-.08-.21-.36-1.04.08-2.16 0 0 .67-.22 2.2.85.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.07 2.2-.85 2.2-.85.44 1.12.16 1.95.08 2.16.51.58.82 1.31.82 2.22 0 3.17-1.87 3.87-3.65 4.08.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.47.55.39A8.24 8.24 0 0 0 16 8.2C16 3.67 12.42 0 8 0z"/></svg>
                </a>
            </div>
        </header>

        <section class="content-area">
            <!-- Runs Section -->
            <div id="runs-section" class="section active">
                <div class="toolbar">
                    <div class="search-container">
                        <input type="text" id="log-search" class="search-input" placeholder="Search runs (task name, project, summary)...">
                    </div>
                    <select id="log-filter-project" class="filter-select">
                        <option value="ALL">All Projects</option>
                    </select>
                    <select id="log-filter-task" class="filter-select">
                        <option value="ALL">All Tasks</option>
                    </select>
                    <select id="log-filter-status" class="filter-select">
                        <option value="ALL">All Status</option>
                        <option value="SUCCESS">Success</option>
                        <option value="FAILED">Failed</option>
                        <option value="RUNNING">Running</option>
                        <option value="ERROR">Error</option>
                        <option value="STOPPED">Stopped</option>
                        <option value="TIMEOUT">Timeout</option>
                    </select>
                    <select id="log-filter-source" class="filter-select">
                        <option value="ALL">All Sources</option>
                        <option value="task">Task</option>
                        <option value="connector_poll">Connector Poll</option>
                    </select>
                </div>
                <div id="logs-container">Loading...</div>
            </div>

            <!-- Config Section -->
            <div id="config-section" class="section">
                <div class="toolbar">
                    <select id="config-filter-project" class="filter-select">
                        <option value="ALL">All Projects</option>
                    </select>
                </div>
                <div id="config-container">Loading...</div>
            </div>

            <!-- Chat Section -->
            <div id="chat-section" class="section">
                <div id="chat-provider-info"></div>
                <div id="chat-history" class="chat-container">
                    <div class="message assistant">Hi! I'm the kage AI assistant. I can help you configure tasks, explain logs, or execute commands.</div>
                </div>
                <div class="chat-input-area">
                    <textarea id="chat-input" placeholder="Type a message..."></textarea>
                    <button id="chat-submit" class="btn" style="height: 60px; padding: 0 24px;">Send</button>
                </div>
            </div>

            <!-- Connectors Section -->
            <div id="connectors-section" class="section">
                <div class="toolbar">
                    <button class="btn" onclick="fetchConnectors()">Refresh</button>
                    <select id="connector-select" class="filter-select" style="margin-left: 10px;" onchange="selectConnector()">
                        <option value="">Select a Connector...</option>
                    </select>
                    <button class="btn" style="margin-left:auto; background:var(--accent-muted); border:1px solid var(--accent); color:var(--accent); font-size:0.85rem;" onclick="showConnectorHelp()">Setup Help</button>
                </div>
                <div id="connectors-container" style="display: flex; gap: 20px; padding: 20px; height: calc(100% - 60px);">
                    <div style="flex: 1; max-width: 400px; display: flex; flex-direction: column;">
                        <div class="card" id="connector-config-card" style="flex-grow: 1; overflow-y: auto;">
                            Select a connector to view details.
                        </div>
                    </div>
                    <div style="flex: 2; display: flex; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-base); border-radius: 8px;">
                        <h3 style="margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--border-base); font-size: 1rem;">Message History</h3>
                        <div id="connector-history" class="chat-container" style="flex-grow: 1; overflow-y: auto;">
                        </div>
                    </div>
                </div>
            </div>
        </section>
    </main>

    <!-- Modal -->
    <div id="help-modal" class="modal">
        <div class="modal-content">
            <div class="modal-header">
                <h3 id="modal-title" style="margin:0;">Setup Help</h3>
                <button class="close-modal" onclick="closeModal()">&times;</button>
            </div>
            <div id="modal-body" class="modal-body">
            </div>
        </div>
    </div>

    <script>
        let lastLogsSignature = null;
        let allLogsData = [];
        let allConfigData = null;
        let allConnectorsData = [];
        let autoScrollEnabled = true;
        let loadedStreams = {};

        // Helper: Get project short name (last directory)
        function getProjectShortName(path) {
            if (!path) return "Unknown";
            const parts = path.split('/');
            return parts[parts.length - 1] || path;
        }

        function escapeHtml(text) {
            if (!text) return "";
            return String(text)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;');
        }

        function renderTaskType(task) {
            if (task.compiled_state === 'fresh') {
                return 'Prompt (<span style="color:var(--success-text);">Compiled</span>)';
            }
            if (task.compiled_state === 'stale') {
                return 'Prompt (<span style="color:var(--error-text);">Compiled</span>)';
            }
            return escapeHtml(task.type_display || (task.prompt ? 'Prompt' : 'Shell'));
        }

        // Navigation
        function navigateTo(section, push = true) {
            document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
            document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
            
            const targetContent = document.getElementById(section + '-section');
            const targetNav = document.getElementById('nav-' + section);
            
            if (targetContent && targetNav) {
                targetContent.classList.add('active');
                targetNav.classList.add('active');
                document.getElementById('page-title').textContent = targetNav.querySelector('.nav-text') ? targetNav.querySelector('.nav-text').textContent.trim() : targetNav.textContent.trim();
                
                if (section === 'config') fetchConfig();
                if (section === 'runs') fetchLogs();
                if (section === 'chat') fetchChatProviderInfo();
                if (section === 'connectors') fetchConnectors();
                
                if (push) {
                    const path = section === 'runs' ? '/runs' : '/' + section;
                    history.pushState({ section }, '', path);
                }
            }
        }

        document.querySelectorAll('.nav-item').forEach(item => {
            item.addEventListener('click', () => {
                const sectionName = item.getAttribute('data-section');
                navigateTo(sectionName);
            });
        });

        window.onpopstate = (event) => {
            if (event.state && event.state.section) {
                navigateTo(event.state.section, false);
            } else {
                // Default to runs if no state
                navigateTo('runs', false);
            }
        };

        // Initial landing
        document.addEventListener('DOMContentLoaded', () => {
            const path = window.location.pathname.substring(1);
            const validSections = ['runs', 'logs', 'config', 'chat', 'connectors'];
            const section = validSections.includes(path) ? (path === 'logs' ? 'runs' : path) : 'runs';
            
            // Sidebar state
            const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
            if (isCollapsed) {
                document.getElementById('sidebar').classList.add('collapsed');
            }

            // Set initial state for the landing page so back button works
            history.replaceState({ section }, '', window.location.pathname);
            navigateTo(section, false);
        });

        document.getElementById('sidebar-toggle').addEventListener('click', () => {
            const sidebar = document.getElementById('sidebar');
            sidebar.classList.toggle('collapsed');
            localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
        });

        function formatDuration(ms) {
            if (ms < 0) ms = 0;
            const sec = Math.floor(ms / 1000);
            const min = Math.floor(sec / 60);
            
            if (min > 0) return `${min}m ${sec % 60}s`;
            return `${sec}s`;
        }

        function updateTimers() {
            document.querySelectorAll('.log-card[data-status="RUNNING"]').forEach(card => {
                const startedAt = new Date(card.getAttribute('data-run-at')).getTime();
                const now = new Date().getTime();
                const durationEl = card.querySelector('.duration-value');
                if (durationEl) {
                    durationEl.textContent = formatDuration(now - startedAt);
                }
            });
        }
        setInterval(updateTimers, 1000);

        async function fetchLogs() {
            try {
                const response = await fetch('/api/runs');
                allLogsData = await response.json();
                updateTaskFilterOptions();
                renderLogs();
            } catch (error) {
                document.getElementById('logs-container').innerHTML = '<p style="color:red">Failed to load runs.</p>';
            }
        }

        function renderLogs() {
            const searchTerm = document.getElementById('log-search').value.toLowerCase();
            const statusFilter = document.getElementById('log-filter-status').value;
            const taskFilter = document.getElementById('log-filter-task').value;
            const projectFilter = document.getElementById('log-filter-project').value;
            const sourceFilter = document.getElementById('log-filter-source').value;
            const container = document.getElementById('logs-container');
            
            const filtered = allLogsData.filter(log => {
                const matchesSearch = !searchTerm || 
                    log.task_name.toLowerCase().includes(searchTerm) ||
                    log.project_path.toLowerCase().includes(searchTerm) ||
                    (log.output_summary && log.output_summary.toLowerCase().includes(searchTerm));
                
                const matchesStatus = statusFilter === 'ALL' || log.status === statusFilter;
                const matchesTask = taskFilter === 'ALL' || log.task_name === taskFilter;
                const matchesProject = projectFilter === 'ALL' || log.project_path === projectFilter;
                const matchesSource = sourceFilter === 'ALL' || log.source === sourceFilter;
                
                return matchesSearch && matchesStatus && matchesTask && matchesProject && matchesSource;
            });

            const signature = JSON.stringify(filtered.map(log => [log.id, log.status, log.run_at, log.finished_at, log.output_summary]));
            if (signature === lastLogsSignature) return;
            lastLogsSignature = signature;

            if (filtered.length === 0) {
                container.innerHTML = '<p style="padding: 20px; color: var(--text-dim);">No runs match your filter.</p>';
                return;
            }

            loadedStreams = {};
            container.innerHTML = '';
            filtered.forEach(log => {
                const card = document.createElement('div');
                card.className = 'log-card';
                card.id = `log-${log.id}`;
                card.setAttribute('data-status', log.status);
                card.setAttribute('data-run-at', log.run_at);

                const start = new Date(log.run_at);
                const end = log.finished_at ? new Date(log.finished_at) : null;
                const duration = end ? (end.getTime() - start.getTime()) : (new Date().getTime() - start.getTime());
                const summary = log.output_summary ? `<div style="margin-top:8px; color:var(--text-secondary); font-size:0.9rem;">${escapeHtml(log.output_summary)}</div>` : '';
                const pathHint = log.has_raw_logs ? 'Raw logs available' : 'Legacy DB-backed run';
                const sourceBadge = log.source === 'connector_poll'
                    ? `<span class="status-badge" style="background:rgba(10,132,255,0.12); color:#0a84ff; border-color:rgba(10,132,255,0.28);">CONNECTOR</span>`
                    : '';

                card.innerHTML = `
                    <div class="log-header" onclick="toggleLogBody('${log.id}')">
                        <div class="log-info">
                            <span class="status-badge ${log.status}">${log.status}</span>
                            ${sourceBadge}
                            <span class="log-name">${escapeHtml(log.task_name)}</span>
                            <span class="log-meta">${start.toLocaleTimeString()} · ${escapeHtml(getProjectShortName(log.project_path))}</span>
                            ${summary}
                            ${log.status === 'RUNNING' ? `<button class="btn-stop" onclick="event.stopPropagation(); stopTask('${log.id}')">Stop</button>` : ''}
                        </div>
                        <div class="log-meta">
                            <span class="duration-value">${formatDuration(duration)}</span>
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
                        </div>
                    </div>
                    <div class="log-body" id="body-${log.id}">
                        <div class="terminal-header">
                            <span>RUN ${log.id.substring(0,8)}</span>
                            <div style="display:flex; gap:8px; align-items:center;">
                                <button class="btn run-stream-btn" data-run-id="${log.id}" data-stream="merged" onclick="event.stopPropagation(); loadRunLog('${log.id}', 'merged', this)">merged</button>
                                <button class="btn run-stream-btn" data-run-id="${log.id}" data-stream="stdout" onclick="event.stopPropagation(); loadRunLog('${log.id}', 'stdout', this)">stdout</button>
                                <button class="btn run-stream-btn" data-run-id="${log.id}" data-stream="stderr" onclick="event.stopPropagation(); loadRunLog('${log.id}', 'stderr', this)">stderr</button>
                            </div>
                        </div>
                        <div style="padding: 0 12px 8px 12px; font-size: 0.8rem; color: var(--text-dim);">Path: <span id="path-${log.id}">${pathHint}</span></div>
                        <div class="terminal" id="term-${log.id}">
                            <p style="margin:0; color:var(--text-dim)">Open a stream to load raw logs.</p>
                        </div>
                    </div>
                `;
                container.appendChild(card);
            });
        }

        function formatTerminalOutput(text) {
            if (!text) {
                return '<p style="margin:0; color:var(--text-dim)">No output recorded.</p>';
            }

            const lines = text.replace(/\\n$/, '').split('\\n');
            return `<div class="lines">` + lines.map((l, i) => `<div><span class="line-numbers">${i+1}</span>${escapeHtml(l)}</div>`).join('') + `</div>`;
        }

        function toggleLogBody(id) {
            const card = document.getElementById(`log-${id}`);
            card.classList.toggle('expanded');
            if (card.classList.contains('expanded') && !loadedStreams[id]) {
                const defaultBtn = card.querySelector(`.run-stream-btn[data-stream="merged"]`);
                loadRunLog(id, 'merged', defaultBtn);
            }
        }

        async function loadRunLog(id, stream, button) {
            const terminal = document.getElementById(`term-${id}`);
            const pathEl = document.getElementById(`path-${id}`);
            if (!terminal) return;

            document.querySelectorAll(`.run-stream-btn[data-run-id="${id}"]`).forEach((btn) => {
                btn.style.background = 'var(--bg-card)';
                btn.style.border = '1px solid var(--border-base)';
                btn.style.color = 'var(--text-secondary)';
            });
            if (button) {
                button.style.background = 'var(--accent-muted)';
                button.style.border = '1px solid var(--accent)';
                button.style.color = 'var(--accent)';
            }

            terminal.innerHTML = '<p style="margin:0; color:var(--text-dim)">Loading raw logs...</p>';
            try {
                const response = await fetch(`/api/runs/${id}/logs?stream=${stream}&lines=400`);
                const payload = await response.json();
                loadedStreams[id] = stream;
                if (pathEl) {
                    pathEl.textContent = payload.path || 'No file path available';
                }
                terminal.innerHTML = formatTerminalOutput(payload.content);
            } catch (error) {
                terminal.innerHTML = '<p style="margin:0; color:var(--error-text)">Failed to load raw logs.</p>';
            }
        }

        function updateTaskFilterOptions() {
            const taskFilter = document.getElementById('log-filter-task');
            const projFilter = document.getElementById('log-filter-project');
            const currentTask = taskFilter.value;
            const currentProj = projFilter.value;
            
            const tasks = [...new Set(allLogsData.map(l => l.task_name))].sort();
            const projects = [...new Set(allLogsData.map(l => l.project_path))].sort();
            
            let tHtml = '<option value="ALL">All Tasks</option>';
            tasks.forEach(t => {
                tHtml += `<option value="${escapeHtml(t)}" ${t === currentTask ? 'selected' : ''}>${escapeHtml(t)}</option>`;
            });
            taskFilter.innerHTML = tHtml;

            let pHtml = '<option value="ALL">All Projects</option>';
            projects.forEach(p => {
                pHtml += `<option value="${escapeHtml(p)}" ${p === currentProj ? 'selected' : ''}>${escapeHtml(getProjectShortName(p))}</option>`;
            });
            projFilter.innerHTML = pHtml;
        }

        document.getElementById('log-search').addEventListener('input', renderLogs);
        document.getElementById('log-filter-status').addEventListener('change', renderLogs);
        document.getElementById('log-filter-task').addEventListener('change', renderLogs);
        document.getElementById('log-filter-project').addEventListener('change', renderLogs);
        document.getElementById('log-filter-source').addEventListener('change', renderLogs);
        document.getElementById('config-filter-project').addEventListener('change', renderConfig);

        // Config & Chat (Simplified for current overhaul focus)
        async function fetchConfig() {
            const response = await fetch('/api/config');
            allConfigData = await response.json();
            
            // Populate config project filter
            const projFilter = document.getElementById('config-filter-project');
            const currentProj = projFilter.value;
            const projects = [...new Set(allConfigData.tasks.map(t => t.project_path))].sort();
            
            let pHtml = '<option value="ALL">All Projects</option>';
            projects.forEach(p => {
                pHtml += `<option value="${escapeHtml(p)}" ${p === currentProj ? 'selected' : ''}>${escapeHtml(getProjectShortName(p))}</option>`;
            });
            projFilter.innerHTML = pHtml;
            
            renderConfig();
        }

        function renderConfig() {
            if (!allConfigData) return;
            const config = allConfigData;
            const container = document.getElementById('config-container');
            const projFilter = document.getElementById('config-filter-project').value;
            
            let tasksHtml = '';
            
            const filteredTasks = config.tasks.filter(t => projFilter === 'ALL' || t.project_path === projFilter);
            
            filteredTasks.forEach(task => {
                let nextRunDisplay = 'Not scheduled';
                if (task.next_run && task.active) {
                    try {
                        const d = new Date(task.next_run);
                        if (!isNaN(d.getTime())) {
                            nextRunDisplay = d.toLocaleString(undefined, {
                                year: 'numeric', month: '2-digit', day: '2-digit',
                                hour: '2-digit', minute: '2-digit', second: '2-digit',
                                timeZoneName: 'short'
                            });
                        } else {
                            nextRunDisplay = task.next_run; // Fallback to raw string
                        }
                    } catch (e) {
                        nextRunDisplay = task.next_run;
                    }
                } else if (!task.active) {
                    nextRunDisplay = 'Task disabled';
                }

                let detailsHtml = '';
                const fields = [
                    { key: 'Type', valHtml: renderTaskType(task) },
                    { key: 'Mode', val: task.mode },
                    { key: 'Concurrency', val: task.concurrency_policy },
                    { key: 'Timeout', val: task.timeout_minutes ? task.timeout_minutes + ' min' : null },
                    { key: 'Allowed Hours', val: task.allowed_hours },
                    { key: 'Denied Hours', val: task.denied_hours },
                    { key: 'Shell', val: task.shell },
                    { key: 'Command', val: task.command },
                    { key: 'Provider', val: task.provider_display }
                ];

                fields.forEach(f => {
                    if (f.valHtml || f.val) {
                        const renderedValue = f.valHtml || escapeHtml(String(f.val));
                        detailsHtml += `<div class="kv-row"><span class="kv-key">${f.key}</span><span class="kv-val">${renderedValue}</span></div>`;
                    }
                });

                if (task.compiled_state && task.compiled_state !== 'n/a') {
                    let compiledValue = 'none';
                    if (task.compiled_state === 'fresh') {
                        compiledValue = `<span style="color:var(--success-text);">fresh</span> ${task.compiled_path ? `<code>${escapeHtml(task.compiled_path)}</code>` : ''}`.trim();
                    } else if (task.compiled_state === 'stale') {
                        compiledValue = `<span style="color:var(--error-text);">stale; recompile required</span> ${task.compiled_path ? `<code>${escapeHtml(task.compiled_path)}</code>` : ''}`.trim();
                    }
                    detailsHtml += `<div class="kv-row"><span class="kv-key">Compiled</span><span class="kv-val">${compiledValue}</span></div>`;
                }

                let promptHtml = '';
                if (task.prompt) {
                    promptHtml = `
                        <div class="prompt-container">
                            <span class="prompt-label">Prompt</span>
                            <div class="prompt-code"><code>${escapeHtml(task.prompt)}</code></div>
                        </div>
                    `;
                }

                tasksHtml += `
                    <div class="card ${task.active ? '' : 'inactive'}" style="margin-bottom: 16px;" id="task-${escapeHtml(task.name)}">
                        <div class="card-header">
                            <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
                                <h4 style="margin:0; font-size: 1rem;">${escapeHtml(task.name)}</h4>
                                ${task.compiled_state === 'fresh' ? '<span class="status-badge" style="background:rgba(35,134,54,0.15); color:var(--success-text);">LOCK FRESH</span>' : ''}
                                ${task.compiled_state === 'stale' ? '<span class="status-badge" style="background:rgba(218,54,51,0.15); color:var(--error-text);">LOCK STALE</span>' : ''}
                                ${task.compiled_state === 'none' ? '<span class="status-badge" style="background:rgba(110,118,129,0.15); color:var(--text-dim);">NO LOCK</span>' : ''}
                            </div>
                            <label class="switch" title="Enable/Disable task">
                                <input type="checkbox" class="task-toggle" 
                                    data-project-path="${escapeHtml(task.project_path)}"
                                    data-task-name="${escapeHtml(task.name)}"
                                    data-file="${escapeHtml(task.file || '')}"
                                    ${task.active ? 'checked' : ''}>
                                <span class="slider"></span>
                            </label>
                        </div>
                        <div class="kv-row"><span class="kv-key">Schedule</span><span class="kv-val"><code>${escapeHtml(task.cron)}</code> <small style="color: var(--text-dim); margin-left: 4px;">(${escapeHtml(task.task_timezone)})</small></span></div>
                        <div class="kv-row"><span class="kv-key">Next Run</span><span class="kv-val">${escapeHtml(nextRunDisplay)}</span></div>
                        <div class="kv-row"><span class="kv-key">Project</span><span class="kv-val">${escapeHtml(getProjectShortName(task.project_path))}</span></div>
                        
                        <div class="task-details-content">
                             ${detailsHtml}
                             ${promptHtml}
                        </div>

                        <button class="task-details-toggle" onclick="this.closest('.card').classList.toggle('expanded')">
                            <span class="details-icon">▼</span> Details
                        </button>

                        <button class="btn" onclick="runTaskNow('${task.project_path}', '${task.name}', '${task.file}')" style="margin-top: 12px; width: 100%; font-size: 0.8rem;" ${task.active ? '' : 'disabled'}>Run Now</button>
                    </div>
                `;
            });

            container.innerHTML = `
                <div class="card" style="margin-bottom: 24px;">
                    <h3 class="card-title">Global Configuration</h3>
                    <div class="kv-row"><span class="kv-key">Default Engine</span><span class="kv-val">${config.default_ai_engine || 'None'}</span></div>
                    <div class="kv-row"><span class="kv-key">Timezone</span><span class="kv-val">${config.timezone}</span></div>
                    <div class="kv-row"><span class="kv-key">UI Port</span><span class="kv-val">${config.ui_port}</span></div>
                </div>
                <h3 class="card-title">Registered Tasks</h3>
                <div class="config-grid">${tasksHtml}</div>
            `;
            wireToggleButtons();
        }

        function wireToggleButtons() {
            document.querySelectorAll('.task-toggle').forEach(toggle => {
                toggle.addEventListener('change', async () => {
                    const projectPath = toggle.getAttribute('data-project-path');
                    const taskName = toggle.getAttribute('data-task-name');
                    const file = toggle.getAttribute('data-file');
                    const active = toggle.checked;

                    const card = toggle.closest('.card');
                    const runBtn = card.querySelector('.btn');

                    try {
                        const response = await fetch('/api/tasks/toggle', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ 
                                project_path: projectPath, 
                                task_name: taskName, 
                                file: file || null,
                                active: active 
                            })
                        });
                        if (!response.ok) throw new Error('Failed to toggle task');

                        if (active) card.classList.remove('inactive');
                        else card.classList.add('inactive');
                        
                        if (runBtn) runBtn.disabled = !active;

                    } catch (error) {
                        alert(error.message);
                        toggle.checked = !active;
                    }
                });
            });
        }

        async function runTaskNow(projectPath, taskName, file) {
            try {
                const response = await fetch('/api/tasks/run-now', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        project_path: projectPath,
                        task_name: taskName,
                        file: file || null
                    })
                });
                if (!response.ok) {
                    const errorData = await response.json().catch(() => ({}));
                    throw new Error(errorData.error || 'Failed to start task');
                }
                // Switch to runs and fetch
                document.getElementById('nav-runs').click();
                setTimeout(fetchLogs, 500);
            } catch (error) {
                alert(error.message);
            }
        }

        async function stopTask(execId) {
            if (!confirm('Are you sure you want to stop this task?')) return;
            try {
                const response = await fetch(`/api/runs/${execId}/stop`, {
                    method: 'POST'
                });
                if (!response.ok) throw new Error('Failed to stop task');
                fetchLogs();
            } catch (error) {
                alert(error.message);
            }
        }

        // Initialize Chat Script (adapted from original)
        const chatInput = document.getElementById('chat-input');
        const chatSubmit = document.getElementById('chat-submit');
        const chatHistory = document.getElementById('chat-history');

        async function fetchChatProviderInfo() {
            try {
                const resp = await fetch('/api/config');
                const config = await resp.json();
                const providerEl = document.getElementById('chat-provider-info');
                if (config.default_ai_engine) {
                    providerEl.innerHTML = `<div class="chat-provider-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"/><circle cx="12" cy="12" r="3"/></svg>Provider: ${escapeHtml(config.default_ai_engine)}</div>`;
                    chatSubmit.disabled = false;
                    chatInput.disabled = false;
                    chatInput.placeholder = 'Type a message...';
                } else {
                    providerEl.innerHTML = `<div class="chat-provider-badge" style="color:var(--error-text); border-color:var(--error); background:rgba(218,54,51,0.08);">⚠ No AI provider configured</div>
<div style="background:var(--bg-card); border:1px solid var(--border-base); border-radius:8px; padding:16px; margin-top:8px; font-size:0.85rem; color:var(--text-secondary);">
    <p style="margin:0 0 12px 0; font-weight:600; color:var(--text-primary);">Setup Guide</p>
    <p style="margin:0 0 8px 0;">Set <code style="background:var(--bg-terminal); padding:2px 6px; border-radius:4px; font-family:'JetBrains Mono',monospace; font-size:0.8rem;">default_ai_engine</code> in <code style="background:var(--bg-terminal); padding:2px 6px; border-radius:4px; font-family:'JetBrains Mono',monospace; font-size:0.8rem;">~/.kage/config.toml</code>:</p>
    <div style="display:flex; flex-direction:column; gap:8px;">
        <div style="background:var(--bg-terminal); padding:10px 12px; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:0.8rem; color:#d1d5db;">
            <div style="color:var(--text-dim); margin-bottom:4px;"># Claude Code</div>
            <div>default_ai_engine = <span style="color:var(--success-text);">"claude"</span></div>
            <div style="color:var(--text-dim); margin-top:4px;"># Install: npm install -g @anthropic-ai/claude-code</div>
        </div>
        <div style="background:var(--bg-terminal); padding:10px 12px; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:0.8rem; color:#d1d5db;">
            <div style="color:var(--text-dim); margin-bottom:4px;"># Gemini CLI</div>
            <div>default_ai_engine = <span style="color:var(--success-text);">"gemini"</span></div>
            <div style="color:var(--text-dim); margin-top:4px;"># Install: npm install -g @anthropic-ai/gemini-cli</div>
        </div>
        <div style="background:var(--bg-terminal); padding:10px 12px; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:0.8rem; color:#d1d5db;">
            <div style="color:var(--text-dim); margin-bottom:4px;"># OpenAI Codex</div>
            <div>default_ai_engine = <span style="color:var(--success-text);">"codex"</span></div>
            <div style="color:var(--text-dim); margin-top:4px;"># Install: npm install -g @openai/codex</div>
        </div>
    </div>
</div>`;
                    chatSubmit.disabled = true;
                    chatInput.disabled = true;
                    chatInput.placeholder = 'Configure a provider to start chatting...';
                }
            } catch(e) {
                console.error('Failed to fetch provider info:', e);
            }
        }
        // Lightweight Markdown renderer
        function renderMarkdown(text) {
            let html = escapeHtml(text);

            // Code blocks (triple backtick)
            html = html.replace(new RegExp('```[a-zA-Z]*\\n([\\s\\S]*?)```', 'g'), function(m, code) {
                return '<pre style="background:var(--bg-terminal); padding:12px; border-radius:6px; border:1px solid var(--border-base); margin:8px 0; overflow-x:auto;"><code>' + code.trim() + '</code></pre>';
            });

            // Inline code
            html = html.replace(/`([^`]+)`/g, '<code style="background:var(--bg-terminal); padding:2px 6px; border-radius:4px; font-size:0.85em;">$1</code>');

            // Headers
            html = html.replace(/^### (.+)$/gm, '<strong style="font-size:1rem; display:block; margin:12px 0 4px;">$1</strong>');
            html = html.replace(/^## (.+)$/gm, '<strong style="font-size:1.1rem; display:block; margin:12px 0 4px;">$1</strong>');
            html = html.replace(/^# (.+)$/gm, '<strong style="font-size:1.2rem; display:block; margin:12px 0 4px;">$1</strong>');

            // Bold and italic
            html = html.replace(/[*][*](.+?)[*][*]/g, '<strong>$1</strong>');
            html = html.replace(/[*](.+?)[*]/g, '<em>$1</em>');

            // Unordered list items
            html = html.replace(/^[-] (.+)$/gm, '<div style="padding-left:16px;">&#8226; $1</div>');

            // Ordered list items
            html = html.replace(/^([0-9]+)[.] (.+)$/gm, '<div style="padding-left:16px;">$1. $2</div>');

            // Line breaks
            html = html.replace(new RegExp('\\n\\n', 'g'), '<br><br>');
            html = html.replace(new RegExp('\\n', 'g'), '<br>');

            return html;
        }

        chatSubmit.onclick = async () => {
            const text = chatInput.value.trim();
            if (!text) return;
            chatInput.value = '';
            chatSubmit.disabled = true;

            const userMsg = document.createElement('div');
            userMsg.className = 'message user';
            userMsg.textContent = text;
            chatHistory.appendChild(userMsg);

            // Show typing indicator
            const typing = document.createElement('div');
            typing.className = 'typing-indicator';
            typing.innerHTML = '<span></span><span></span><span></span>';
            chatHistory.appendChild(typing);
            chatHistory.scrollTop = chatHistory.scrollHeight;

            try {
                const response = await fetch('/api/chat', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message: text })
                });
                const result = await response.json();

                // Remove typing indicator
                typing.remove();

                const botMsg = document.createElement('div');
                botMsg.className = 'message assistant';
                if (result.error) {
                    botMsg.innerHTML = `<span style="color:var(--error-text)">Error: ${escapeHtml(result.error)}</span>`;
                } else if (result.stdout) {
                    botMsg.innerHTML = renderMarkdown(result.stdout);
                } else {
                    botMsg.textContent = 'Command executed (no output).';
                }
                chatHistory.appendChild(botMsg);
            } catch (e) {
                typing.remove();
                const errMsg = document.createElement('div');
                errMsg.className = 'message assistant';
                errMsg.innerHTML = `<span style="color:var(--error-text)">Network error: ${escapeHtml(e.message)}</span>`;
                chatHistory.appendChild(errMsg);
            }

            chatSubmit.disabled = false;
            chatHistory.scrollTop = chatHistory.scrollHeight;
        };

        chatInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                chatSubmit.click();
            }
        });

        // Polling
        fetchLogs();
        setInterval(() => {
            if (document.getElementById('runs-section').classList.contains('active')) {
                fetchLogs();
            }
        }, 5000);
        async function fetchConnectors() {
            try {
                const res = await fetch('/api/connectors');
                allConnectorsData = await res.json();
                renderConnectorsSelect();
            } catch (err) {
                console.error("Failed to load connectors", err);
            }
        }

        function closeModal() {
            document.getElementById('help-modal').classList.remove('active');
        }

        const SETUP_GUIDES = {
            discord: `
# Discord Setup Guide
1. **Developer Portal**: Create app at [discord.com/developers](https://discord.com/developers/applications).
2. **Bot Token**: Reset Token in **Bot** tab. Enable **Message Content Intent**.
3. **OAuth2**: URL Generator -> \\`bot\\` -> \\`Send Messages\\`, \\`Read Message History\\`.
4. **Channel ID**: Enable Developer Mode in Discord, right-click channel -> **Copy Channel ID**.
5. **Config**:
\\`\\`\\`toml
[connectors.my_discord]
type = "discord"
poll = true  # ⚠️ Only in private/trusted channels
bot_token = "..."
channel_id = "..."
\\`\\`\\`

⚠️ **Security**: \\`poll = true\\` grants channel members AI access to your PC. Task notifications work even with \\`poll = false\\`.
`,
            slack: `
# Slack Setup Guide
1. **Slack API**: Create app at [api.slack.com/apps](https://api.slack.com/apps) (From scratch).
2. **Scopes**: OAuth & Permissions -> \\`channels:history\\`, \\`chat:write\\`.
3. **Install**: Install to Workspace, copy **Bot User OAuth Token** (\\`xoxb-...\\`).
4. **Channel ID**: Channel details -> find ID at bottom (starts with \\`C\\`).
5. **Invite**: Type \\`/invite @YourBotName\\` in the channel.
6. **Config**:
\\`\\`\\`toml
[connectors.my_slack]
type = "slack"
poll = true  # ⚠️ Only in private/trusted channels
bot_token = "xoxb-..."
channel_id = "..."
\\`\\`\\`

⚠️ **Security**: \\`poll = true\\` grants channel members AI access to your PC. Task notifications work even with \\`poll = false\\`.
`,
            telegram: `
# Telegram Setup Guide
1. **BotFather**: Message [@BotFather](https://t.me/BotFather) on Telegram, send \\`/newbot\\`.
2. **Bot Token**: Copy the token BotFather gives you (e.g. \\`123456:ABC-DEF...\\`).
3. **Chat ID**: Add bot to your group/DM, send a message, then visit:
   \\`https://api.telegram.org/bot<TOKEN>/getUpdates\\` — find \\`chat.id\\`.
4. **Config**:
\\`\\`\\`toml
[connectors.my_telegram]
type = "telegram"
poll = true  # ⚠️ Only in private/trusted chats
bot_token = "..."
chat_id = "..."
\\`\\`\\`

⚠️ **Security**: \\`poll = true\\` grants chat members AI access to your PC. Task notifications work even with \\`poll = false\\`.
`
        };

        function showConnectorHelp() {
            const select = document.getElementById('connector-select');
            const name = select.value;
            let type = 'discord';
            
            if (name) {
                const c = allConnectorsData.find(x => x.name === name);
                if (c) type = c.config.type;
            }
            
            const modal = document.getElementById('help-modal');
            const body = document.getElementById('modal-body');
            const title = document.getElementById('modal-title');
            
            const guide = SETUP_GUIDES[type] || "# Guide not available\\nAvailable types: discord, slack, telegram";
            title.textContent = "Setup Help: " + type.charAt(0).toUpperCase() + type.slice(1);
            body.innerHTML = renderMarkdown(guide);
            modal.classList.add('active');
        }

        function renderConnectorsSelect() {
            const select = document.getElementById('connector-select');
            const currentVal = select.value;
            let html = '<option value="">Select a Connector...</option>';
            allConnectorsData.forEach(c => {
                html += `<option value="${escapeHtml(c.name)}">${escapeHtml(c.name)} (${escapeHtml(c.config.type)})</option>`;
            });
            select.innerHTML = html;
            if (currentVal && allConnectorsData.some(c => c.name === currentVal)) {
                select.value = currentVal;
            } else if (allConnectorsData.length > 0) {
                select.value = allConnectorsData[0].name;
            }
            selectConnector();
        }

        async function selectConnector() {
            const name = document.getElementById('connector-select').value;
            const configCard = document.getElementById('connector-config-card');
            const historyContainer = document.getElementById('connector-history');
            
            if (!name) {
                configCard.innerHTML = 'Select a connector to view details.';
                historyContainer.innerHTML = '';
                return;
            }
            
            const connector = allConnectorsData.find(c => c.name === name);
            if (!connector) return;
            
            // Render Config
            let confHtml = `<h3 style="margin:0 0 10px 0;">${escapeHtml(connector.name)}</h3>`;
            Object.entries(connector.config).forEach(([k, v]) => {
                confHtml += `<div class="kv-row"><span class="kv-key">${escapeHtml(k)}</span><span class="kv-val">${escapeHtml(String(v))}</span></div>`;
            });
            configCard.innerHTML = confHtml;
            
            // Render History
            historyContainer.innerHTML = 'Loading history...';
            try {
                const res = await fetch(`/api/connectors/${encodeURIComponent(name)}/history`);
                const history = await res.json();
                
                if (history.length === 0) {
                    historyContainer.innerHTML = '<div style="padding:16px; color:var(--text-dim);">No history found.</div>';
                    return;
                }
                
                let hHtml = '';
                history.forEach(entry => {
                    const date = new Date(entry.timestamp * 1000).toLocaleString();
                    const isAssistant = entry.role.toLowerCase() === 'assistant';
                    const roleClass = isAssistant ? 'assistant' : 'user';
                    const runLink = entry.run_id
                        ? `<button class="btn" style="margin-left:8px; font-size:0.75rem; padding:4px 8px;" onclick="openRunFromHistory('${escapeHtml(entry.run_id)}')">View run</button>`
                        : '';
                    hHtml += `
                        <div style="margin-bottom: 4px; font-size: 0.8rem; color: var(--text-dim); text-align: ${isAssistant ? 'left' : 'right'}">
                            ${escapeHtml(entry.role)} · ${date}${runLink}
                        </div>
                        <div class="message ${roleClass}">${renderMarkdown(entry.content)}</div>
                    `;
                });
                historyContainer.innerHTML = hHtml;
                historyContainer.scrollTop = historyContainer.scrollHeight;
            } catch (err) {
                historyContainer.innerHTML = '<div style="color:var(--error-text);">Failed to load history</div>';
            }
        }

        async function openRunFromHistory(runId) {
            navigateTo('runs');
            await fetchLogs();
            const card = document.getElementById(`log-${runId}`);
            if (!card) return;
            card.scrollIntoView({ behavior: 'smooth', block: 'center' });
            if (!card.classList.contains('expanded')) {
                toggleLogBody(runId);
            }
        }
    </script>
</body>
</html>
"""


class ChatRequest(BaseModel):
    message: str


class RunTaskRequest(BaseModel):
    project_path: str
    task_name: str
    file: str | None = None


class ToggleTaskRequest(BaseModel):
    project_path: str
    task_name: str
    active: bool
    file: str | None = None


@app.get("/", response_class=HTMLResponse)
@app.get("/runs", response_class=HTMLResponse)
@app.get("/logs", response_class=HTMLResponse)
@app.get("/config", response_class=HTMLResponse)
@app.get("/chat", response_class=HTMLResponse)
def root():
    return INDEX_HTML


@app.get("/api/runs")
def get_runs(limit: int = 50, source: str | None = None):
    from .runs import list_runs

    return [run.to_dict() for run in list_runs(limit=limit, source=source)]


@app.get("/api/logs")
def get_logs(limit: int = 50, source: str | None = None):
    return get_runs(limit=limit, source=source)


@app.get("/api/runs/{exec_id}")
def get_run_api(exec_id: str):
    from .runs import get_run, load_run_metadata

    run = get_run(exec_id)
    if not run:
        return JSONResponse(status_code=404, content={"error": "Run not found."})
    payload = run.to_dict()
    payload["metadata"] = load_run_metadata(run)
    return payload


@app.get("/api/runs/{exec_id}/logs")
def get_run_logs(
    exec_id: str,
    stream: str = "merged",
    lines: int | None = None,
    since: str | None = None,
    format: str = "json",
):
    from .runs import get_run, load_log_text, log_path_for_stream

    if stream not in {"merged", "stdout", "stderr"}:
        return JSONResponse(status_code=400, content={"error": "Invalid stream."})

    run = get_run(exec_id)
    if not run:
        return JSONResponse(status_code=404, content={"error": "Run not found."})

    try:
        content = load_log_text(run, stream=stream, lines=lines, since=since)
    except ValueError as exc:
        return JSONResponse(status_code=400, content={"error": str(exc)})
    path = log_path_for_stream(run, stream)

    payload = {
        "run": run.to_dict(),
        "stream": stream,
        "path": str(path) if path else None,
        "content": content,
    }
    if format == "text":
        return PlainTextResponse(content)
    return payload


@app.get("/api/config")
def get_config_api():
    from .compiler import compiled_task_indicator
    from .scheduler import get_projects
    from .parser import load_project_tasks
    from datetime import datetime, timezone as dt_timezone
    import zoneinfo
    from croniter import croniter

    config = get_global_config()
    projects = get_projects()

    # Setup timezone for calculating next run
    tz_name = config.timezone
    try:
        tz = zoneinfo.ZoneInfo(tz_name)
    except Exception:
        tz = dt_timezone.utc
    all_tasks = []
    for proj_dir in projects:
        merged_cfg = get_global_config(workspace_dir=proj_dir)
        tasks = load_project_tasks(proj_dir)
        for toml_path, task_def in tasks:
            t = task_def.task
            compiled = compiled_task_indicator(t, toml_path)
            explicit_provider = t.provider or (
                t.ai.engine if (t.ai and t.ai.engine) else None
            )
            effective_provider = explicit_provider or merged_cfg.default_ai_engine
            provider_display = None
            if t.prompt and not t.command:
                provider_display = (
                    effective_provider
                    if explicit_provider or not effective_provider
                    else f"{effective_provider} (Inherited)"
                )
            type_display = "Shell"
            if t.prompt and not t.command:
                type_display = (
                    "Prompt (Compiled)"
                    if compiled["state"] in {"fresh", "stale"}
                    else "Prompt"
                )

            # Use task-specific timezone if defined, otherwise fallback to global
            task_tz = tz
            if t.timezone:
                try:
                    task_tz = zoneinfo.ZoneInfo(t.timezone)
                except Exception:
                    pass

            task_now = datetime.now(task_tz)
            next_run_str = ""
            try:
                itr = croniter(t.cron, task_now)
                next_dt = itr.get_next(datetime)
                # Return ISO format for frontend to parse in browser timezone
                next_run_str = next_dt.isoformat()
            except Exception:
                next_run_str = "Invalid cron"

            all_tasks.append(
                {
                    "name": t.name,
                    "cron": t.cron,
                    "active": t.active,
                    "next_run": next_run_str,
                    "prompt": t.prompt,
                    "command": t.command,
                    "shell": t.shell,
                    "mode": t.mode,
                    "concurrency_policy": t.concurrency_policy,
                    "timeout_minutes": t.timeout_minutes,
                    "allowed_hours": t.allowed_hours,
                    "denied_hours": t.denied_hours,
                    "provider": t.provider
                    or (t.ai.engine if (t.ai and t.ai.engine) else None),
                    "provider_display": provider_display,
                    "type_display": type_display,
                    "project_path": str(proj_dir),
                    "project_name": proj_dir.name,
                    "file": str(toml_path),
                    "task_timezone": str(task_tz),
                    "compiled_state": compiled["state"],
                    "compiled_path": compiled["path"],
                    "compiled_needs_compile": compiled["needs_compile"],
                }
            )

    return {
        "default_ai_engine": config.default_ai_engine,
        "log_level": config.log_level,
        "ui_port": config.ui_port,
        "timezone": config.timezone,
        "tasks": all_tasks,
    }


@app.post("/api/tasks/run-now")
def run_task_now(req: RunTaskRequest):
    from pathlib import Path
    from fastapi.responses import JSONResponse
    from .parser import load_project_tasks
    from .executor import execute_task

    proj_dir = Path(req.project_path).expanduser().resolve()
    if not proj_dir.exists():
        return JSONResponse(
            status_code=404,
            content={"error": f"Project path not found: {proj_dir}"},
        )

    try:
        tasks = load_project_tasks(proj_dir)
        matched = None
        req_file = str(Path(req.file).expanduser().resolve()) if req.file else None
        for task_file, task_def in tasks:
            if task_def.task.name != req.task_name:
                continue
            if req_file and str(task_file.resolve()) != req_file:
                continue
            matched = (task_file, task_def.task)
            break

        if matched is None:
            return JSONResponse(
                status_code=404,
                content={"error": f"Task '{req.task_name}' not found."},
            )

        task_file, task = matched
        execute_task(proj_dir, task, task_file=task_file)
        return {"message": f"Task '{req.task_name}' started."}
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


@app.post("/api/runs/{exec_id}/stop")
def stop_run_api(exec_id: str):
    from .executor import stop_execution

    stop_execution(exec_id)
    return {"message": "Stop signal sent."}


@app.post("/api/executions/{exec_id}/stop")
def stop_execution_api(exec_id: str):
    return stop_run_api(exec_id)


@app.post("/api/tasks/toggle")
def toggle_task(req: ToggleTaskRequest):
    import tomlkit
    import re
    from pathlib import Path

    task_file = Path(req.file) if req.file else None
    if not task_file or not task_file.exists():
        return JSONResponse(status_code=404, content={"error": "Task file not found."})

    try:
        content = task_file.read_text(encoding="utf-8")
        if task_file.suffix == ".md":
            new_val = "true" if req.active else "false"
            if "active:" in content:
                content = re.sub(
                    r"active:\s*(true|false)", f"active: {new_val}", content
                )
            else:
                content = re.sub(r"(cron:.*?\n)", rf"\1active: {new_val}\n", content)
        else:
            doc = tomlkit.load(content)
            if "task" in doc:
                doc["task"]["active"] = req.active
            else:
                for k, v in doc.items():
                    if k.startswith("task") and isinstance(v, dict):
                        if v.get("name") == req.task_name:
                            v["active"] = req.active
            content = tomlkit.dumps(doc)

        task_file.write_text(content, encoding="utf-8")
        return {
            "message": f"Task '{req.task_name}' {'enabled' if req.active else 'disabled'}."
        }
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


@app.post("/api/chat")
def handle_chat(req: ChatRequest):
    from fastapi.responses import JSONResponse

    try:
        reply_data = generate_chat_reply(req.message)
        # Clean thinking tags for the UI too
        reply_data["stdout"] = clean_ai_reply(reply_data.get("stdout", ""))
        return reply_data
    except ValueError as e:
        return JSONResponse(status_code=400, content={"error": str(e)})
    except Exception as e:
        return JSONResponse(status_code=500, content={"error": str(e)})


def open_browser(url: str):
    time.sleep(1)  # wait for server to start
    webbrowser.open(url)


def start_ui(host: str = "127.0.0.1", port: int = 8080):
    url = f"http://{host}:{port}"
    threading.Thread(target=open_browser, args=(url,), daemon=True).start()
    uvicorn.run(app, host=host, port=port)


@app.get("/api/connectors")
def get_connectors():
    config = get_global_config()
    res = []
    for name, c in config.connectors.items():
        masked_c = dict(c)
        if "bot_token" in masked_c and masked_c["bot_token"]:
            masked_c["bot_token"] = "***" + masked_c["bot_token"][-4:]
        res.append({"name": name, "config": masked_c})
    return res


@app.get("/api/connectors/{name}/history")
def get_connector_history(name: str):
    import json

    history_file = Path.home() / ".kage" / "connectors" / f"{name}_history.jsonl"
    if not history_file.exists():
        return []

    entries = []
    try:
        with history_file.open("r", encoding="utf-8") as f:
            for line in f:
                if not line.strip():
                    continue
                try:
                    entries.append(json.loads(line))
                except json.JSONDecodeError:
                    pass
    except Exception as e:
        print(f"Error reading history for {name}: {e}")
    # Return last 50 messages
    return entries[-50:]
