Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chashchin-dmitry_pocketcoder-a1.json

1 line
134 KiB
JSON

{"repo": "chashchin-dmitry/pocketcoder-a1", "files": {"a1/config.py": {"80863f2ff11776214b3cf361c01077c273a14e56": {"t": 1785674422.3965702, "n": 3929, "h": "0028ed2254f968b0", "c": "\"\"\"\na1/config.py — Configuration management for PocketCoder-A1.\n\nLoads/saves .a1/config.json with priority resolution:\n CLI args > env vars > config.json > defaults\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any, Optional\n\nDEFAULTS = {\n \"provider\": \"claude-max\",\n \"model\": None,\n \"api_key\": None,\n \"ollama_host\": \"http://localhost:11434\",\n \"ollama_model\": \"qwen3:30b-a3b\",\n \"max_sessions\": 100,\n \"max_turns\": 25,\n \"session_delay\": 5,\n \"context_threshold\": 0.70,\n}\n\n# Env var → config key mapping\nENV_MAP = {\n \"ANTHROPIC_API_KEY\": \"api_key\",\n \"OLLAMA_HOST\": \"ollama_host\",\n \"OLLAMA_MODEL\": \"ollama_model\",\n}\n\n\nclass Config:\n \"\"\"Configuration manager — load/save/resolve .a1/config.json.\"\"\"\n\n def __init__(self, project_dir: Path):\n self.project_dir = Path(project_dir)\n self.path = self.project_dir / \".a1\" / \"config.json\"\n self._data: dict = {}\n self.load()\n\n def load(self) -> dict:\n \"\"\"Load from disk. Returns stored values (not merged with defaults).\"\"\"\n if self.path.exists():\n try:\n self._data = json.loads(self.path.read_text(encoding=\"utf-8\"))\n except (json.JSONDecodeError, OSError):\n self._data = {}\n else:\n self._data = {}\n return self._data\n\n def save(self) -> None:\n \"\"\"Save current data to .a1/config.json.\"\"\"\n self.path.parent.mkdir(parents=True, exist_ok=True)\n self.path.write_text(\n json.dumps(self._data, indent=2, ensure_ascii=False) + \"\\n\",\n encoding=\"utf-8\",\n )\n\n def get(self, key: str, default: Any = None) -> Any:\n \"\"\"Get value with fallback: config → DEFAULTS → default.\"\"\"\n if key in self._data and self._data[key] is not None:\n return self._data[key]\n if key in DEFAULTS and DEFAULTS[key] is not None:\n return DEFAULTS[key]\n return default\n\n def set(self, key: str, value: Any) -> None:\n \"\"\"Set a value and save to disk.\"\"\"\n self._data[key] = value\n self.save()\n\n def get_all(self) -> dict:\n \"\"\"Get full config merged with defaults.\"\"\"\n result = dict(DEFAULTS)\n result.update({k: v for k, v in self._data.items() if v is not None})\n return result\n\n def reset(self) -> None:\n \"\"\"Reset to defaults.\"\"\"\n self._data = {}\n self.save()\n\n def resolve(self, cli_args: Optional[dict] = None) -> dict:\n \"\"\"Merge all sources: CLI > env > config > defaults.\n\n Returns dict with keys matching SessionLoop.__init__ params:\n provider, model, api_key, ollama_host, ollama_model,\n max_sessions, max_turns, session_delay\n \"\"\"\n # 1. Start with defaults\n result = dict(DEFAULTS)\n\n # 2. Overlay config.json values\n for k, v in self._data.items():\n if v is not None:\n result[k] = v\n\n # 3. Overlay env vars\n for env_var, config_key in ENV_MAP.items():\n val = os.environ.get(env_var)\n if val:\n result[config_key] = val\n\n # 4. Overlay CLI args (skip None values — means \"not provided\")\n if cli_args:\n for k, v in cli_args.items():\n if v is not None:\n result[k] = v\n\n # Remove context_threshold from result (it's not a SessionLoop param)\n # Store it separately so loop.py can access if needed\n result.pop(\"context_threshold\", None)\n\n return result\n\n def mask_api_key(self, key: Optional[str] = None) -> Optional[str]:\n \"\"\"Mask API key for display: sk-ant-api03-...xxxx.\"\"\"\n k = key or self.get(\"api_key\")\n if not k or len(k) < 8:\n return k\n return k[:10] + \"...\" + k[-4:]\n\n def __repr__(self) -> str:\n return f\"Config({self.path}, keys={list(self._data.keys())})\"\n"}}, "a1/dashboard.py": {"80863f2ff11776214b3cf361c01077c273a14e56": {"t": 1785674429.3935359, "n": 127348, "h": "955a3f4285b597c7", "c": "#!/usr/bin/env python3\n\"\"\"\nPocketCoder-A1 Dashboard — Full-featured Web UI\n\"\"\"\n\nimport html as html_mod\nimport json\nimport re\nimport socket\nimport threading\nimport time as _time\nimport webbrowser\nfrom datetime import datetime\nfrom http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer\nfrom pathlib import Path\nfrom string import Template\nfrom urllib.parse import parse_qs, urlparse\n\nfrom .checkpoint import CheckpointManager\nfrom .tasks import TaskManager\n\nPROJECT_DIR = None\nAGENT_RUNNING = False\nAGENT_LOOP = None # Reference to SessionLoop for stop control\nACTIVITY_LOG = [] # Live activity log\nAGENT_LOG_BUFFER = [] # Live agent output lines for dashboard\n\n# Cache current task_id for tagging log entries (refreshes every 2s)\n_TASK_CACHE = {\"task_id\": None, \"ts\": 0}\n\n\ndef _get_current_task_id():\n \"\"\"Get current task_id from checkpoint (cached 2s)\"\"\"\n now = _time.time()\n if now - _TASK_CACHE[\"ts\"] < 2.0:\n return _TASK_CACHE[\"task_id\"]\n _TASK_CACHE[\"ts\"] = now\n if AGENT_LOOP and hasattr(AGENT_LOOP, 'checkpoint'):\n try:\n cp = AGENT_LOOP.checkpoint.load()\n _TASK_CACHE[\"task_id\"] = cp.get(\"current_task\")\n except Exception:\n pass\n return _TASK_CACHE[\"task_id\"]\n\n\ndef esc(text: str) -> str:\n \"\"\"Escape HTML to prevent XSS\"\"\"\n return html_mod.escape(str(text)) if text else \"\"\n\n\ndef log_activity(action: str, details: str = \"\", status: str = \"info\"):\n \"\"\"Log activity for dashboard\"\"\"\n ACTIVITY_LOG.append({\n \"time\": datetime.now().strftime(\"%H:%M:%S\"),\n \"action\": action,\n \"details\": details,\n \"status\": status # info, success, error, warning\n })\n # Keep last 100 entries\n if len(ACTIVITY_LOG) > 100:\n ACTIVITY_LOG.pop(0)\n\n\ndef _classify_line(line: str) -> str:\n \"\"\"Classify a Claude output line into a tool type for icon display\"\"\"\n lower = line.lower().strip()\n if any(kw in lower for kw in [\"read(\", \"reading file\", \"read file\", \"reading \", \"read \"]):\n return \"read\"\n elif any(kw in lower for kw in [\"edit(\", \"editing file\", \"edit file\", \"editing \"]):\n return \"edit\"\n elif any(kw in lower for kw in [\"write(\", \"writing file\", \"write file\", \"creating file\", \"created \"]):\n return \"write\"\n elif any(kw in lower for kw in [\"bash(\", \"running:\", \"$ \", \"command\", \"terminal\", \"pytest\", \"ruff \"]):\n return \"bash\"\n elif any(kw in lower for kw in [\"let me\", \"i'll\", \"i need\", \"thinking\", \"analyzing\", \"looking\"]):\n return \"thinking\"\n return \"text\"\n\n\ndef _on_agent_line(line: str, event_type: str = None):\n \"\"\"Parse agent output line and add to live buffer.\n event_type: pre-classified type from stream-json parser\n (read/edit/write/bash/thinking/text/metric/verify)\n If not provided, falls back to heuristic classifier.\"\"\"\n stripped = line.rstrip(\"\\n\")\n if not stripped:\n return\n entry = {\n \"time\": datetime.now().strftime(\"%H:%M:%S\"),\n \"line\": stripped,\n \"type\": event_type if event_type else _classify_line(line),\n \"task_id\": _get_current_task_id(),\n }\n AGENT_LOG_BUFFER.append(entry)\n if len(AGENT_LOG_BUFFER) > 500:\n AGENT_LOG_BUFFER.pop(0)\n\n\nCSS = '''\n:root {\n --bg-primary: #FAFAF9;\n --bg-secondary: #FFFFFF;\n --bg-tertiary: #F0EFED;\n --text-primary: #1A1A1A;\n --text-secondary: #6B6B6B;\n --border-color: #E5E3DF;\n --accent: #DA7756;\n --accent-light: #E8956A;\n --success: #2E8B57;\n --warning: #D4940A;\n --danger: #D94F4F;\n}\n\n[data-theme=\"dark\"] {\n --bg-primary: #191919;\n --bg-secondary: #262626;\n --bg-tertiary: #1E1E1E;\n --text-primary: #ECECEC;\n --text-secondary: #8E8E8E;\n --border-color: #333333;\n}\n\n* { box-sizing: border-box; margin: 0; padding: 0; }\n\nbody {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n}\n\n.layout {\n display: grid;\n grid-template-columns: 240px 1fr;\n min-height: 100vh;\n}\n\n/* Sidebar */\n.sidebar {\n background: var(--bg-secondary);\n border-right: 3px solid var(--border-color);\n padding: 20px;\n position: sticky;\n top: 0;\n height: 100vh;\n overflow-y: auto;\n}\n\n.logo-ascii {\n margin-bottom: 4px;\n user-select: none;\n}\n.logo-art {\n color: #E8956A;\n font-size: 14px;\n line-height: 1.1;\n font-family: 'JetBrains Mono', monospace;\n margin: 0;\n text-shadow:\n 1px 1px 0 #DA7756,\n 2px 2px 0 #C4653F,\n 3px 3px 0 #A8522F,\n 4px 4px 0 #8C4020,\n 5px 5px 0 #703018;\n letter-spacing: 1px;\n}\n[data-theme=\"dark\"] .logo-art {\n color: #F0A87A;\n text-shadow:\n 1px 1px 0 #DA7756,\n 2px 2px 0 #C4653F,\n 3px 3px 0 #A8522F,\n 4px 4px 0 #8C4020,\n 5px 5px 0 #703018;\n}\n\n.logo-sub {\n font-size: 10px;\n color: var(--text-secondary);\n margin-bottom: 30px;\n font-family: 'JetBrains Mono', monospace;\n letter-spacing: 1px;\n text-transform: uppercase;\n}\n\n.nav-section {\n margin-bottom: 24px;\n}\n\n.nav-title {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: var(--text-secondary);\n margin-bottom: 12px;\n}\n\n.nav-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border-radius: 0;\n color: var(--text-primary);\n text-decoration: none;\n margin-bottom: 2px;\n transition: background 0.15s;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n font-weight: 500;\n border-left: 3px solid transparent;\n}\n\n.nav-item:hover {\n background: var(--bg-tertiary);\n border-left-color: var(--border-color);\n}\n\n.nav-item.active {\n background: var(--accent);\n color: white;\n border-left-color: var(--accent);\n}\n\n.nav-item i {\n width: 18px;\n text-align: center;\n}\n\n/* Main content */\n.main {\n padding: 24px 32px;\n overflow-y: auto;\n}\n\n.header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.page-title {\n font-size: 20px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 1px;\n}\n\n.header-actions {\n display: flex;\n gap: 12px;\n align-items: center;\n}\n\n/* Status badge */\n.status {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border-radius: 0;\n font-size: 11px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 1px;\n}\n\n.status-running { background: var(--success); color: white; }\n.status-stopped { background: var(--danger); color: white; }\n.status-completed { background: var(--accent); color: white; }\n\n/* Cards */\n.cards {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 16px;\n margin-bottom: 24px;\n}\n\n.card {\n background: var(--bg-secondary);\n border: 3px solid var(--border-color);\n border-radius: 0;\n padding: 20px;\n transition: transform 0.15s;\n box-shadow: inset 2px 2px 6px rgba(0,0,0,0.08);\n}\n.card:hover {\n transform: translateY(-1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.04);\n}\n[data-theme=\"dark\"] .card:hover {\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.15);\n}\n\n.card-title {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--text-secondary);\n margin-bottom: 8px;\n display: flex;\n align-items: center;\n gap: 6px;\n font-family: 'JetBrains Mono', monospace;\n font-weight: 700;\n}\n\n.card-value {\n font-size: 28px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n}\n\n.card-sub {\n font-size: 11px;\n color: var(--text-secondary);\n margin-top: 4px;\n font-family: 'JetBrains Mono', monospace;\n}\n\n/* Progress bar */\n.progress {\n height: 4px;\n background: var(--bg-tertiary);\n border-radius: 0;\n margin-top: 12px;\n overflow: hidden;\n border: 1px solid var(--border-color);\n}\n\n.progress-fill {\n height: 100%;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s;\n}\n\n/* Tasks list */\n/* --- Stalinist Grid --- */\n.task-list {\n background: var(--bg-tertiary);\n border: 3px solid var(--border-color);\n border-radius: 0;\n overflow: hidden;\n}\n\n.task-header {\n padding: 14px 20px;\n border-bottom: 3px solid var(--border-color);\n display: flex;\n justify-content: space-between;\n align-items: center;\n background: var(--bg-secondary);\n}\n\n.task-header h3 {\n font-size: 14px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n font-family: 'JetBrains Mono', monospace;\n}\n\n.task-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n gap: 0;\n padding: 0;\n}\n\n/* Default task = horizontal row (dashboard list) */\n.task {\n display: flex;\n flex-direction: row;\n align-items: center;\n padding: 14px 16px;\n border-bottom: 1px solid var(--border-color);\n transition: background 0.15s;\n position: relative;\n background: var(--bg-secondary);\n gap: 12px;\n}\n\n.task:last-child {\n border-bottom: none;\n}\n\n.task:hover {\n background: var(--bg-tertiary);\n}\n\n/* Grid card = column layout (tasks page) */\n.task-grid .task {\n flex-direction: column;\n align-items: stretch;\n padding: 16px;\n border-right: 3px solid var(--border-color);\n border-bottom: 3px solid var(--border-color);\n min-height: 130px;\n box-shadow: inset 2px 2px 6px rgba(0,0,0,0.12);\n gap: 0;\n}\n\n.task-grid .task:hover {\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.06);\n}\n\n.task-status-stripe {\n position: absolute;\n top: 0;\n left: 0;\n width: 4px;\n height: 100%;\n}\n.task-status-stripe.pending { background: var(--border-color); }\n.task-status-stripe.in_progress { background: var(--warning); }\n.task-status-stripe.done { background: var(--success); }\n.task-status-stripe.blocked { background: var(--danger); }\n\n.task-check {\n width: 20px;\n height: 20px;\n border-radius: 0;\n border: 2px solid var(--border-color);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 11px;\n flex-shrink: 0;\n}\n\n.task-check.done {\n background: var(--success);\n border-color: var(--success);\n color: white;\n}\n\n.task-check.progress {\n background: var(--warning);\n border-color: var(--warning);\n color: white;\n}\n\n.task-check.blocked {\n background: var(--danger);\n border-color: var(--danger);\n color: white;\n}\n\n.task-top-row {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 10px;\n}\n\n.task-content {\n flex: 1;\n}\n\n.task-title {\n font-weight: 600;\n font-size: 13px;\n margin-bottom: 6px;\n line-height: 1.3;\n font-family: 'JetBrains Mono', monospace;\n}\n\n.task-meta {\n font-size: 11px;\n color: var(--text-secondary);\n font-family: 'JetBrains Mono', monospace;\n}\n\n.task-desc-preview {\n font-size: 11px;\n color: var(--text-secondary);\n margin-top: 8px;\n line-height: 1.4;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.task-card-footer {\n margin-top: auto;\n padding-top: 10px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 11px;\n color: var(--text-secondary);\n}\n.task-card-footer i {\n font-size: 12px;\n color: var(--accent);\n}\n\n.task-phase {\n background: var(--bg-tertiary);\n padding: 2px 8px;\n border-radius: 0;\n border: 1px solid var(--border-color);\n font-size: 10px;\n color: var(--text-secondary);\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* Activity log */\n.activity {\n background: var(--bg-secondary);\n border: 3px solid var(--border-color);\n border-radius: 0;\n margin-top: 24px;\n}\n\n.activity-header {\n padding: 14px 20px;\n border-bottom: 3px solid var(--border-color);\n font-weight: 700;\n font-size: 13px;\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n}\n\n.activity-list {\n max-height: 300px;\n overflow-y: auto;\n}\n\n.activity-item {\n display: flex;\n gap: 12px;\n padding: 12px 20px;\n border-bottom: 1px solid var(--border-color);\n font-size: 13px;\n}\n\n.activity-item:last-child {\n border-bottom: none;\n}\n\n.activity-time {\n color: var(--text-secondary);\n font-family: monospace;\n flex-shrink: 0;\n}\n\n.activity-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-top: 5px;\n flex-shrink: 0;\n}\n\n.activity-dot.info { background: var(--accent); }\n.activity-dot.success { background: var(--success); }\n.activity-dot.warning { background: var(--warning); }\n.activity-dot.error { background: var(--danger); }\n\n.activity-text {\n flex: 1;\n}\n\n.activity-details {\n color: var(--text-secondary);\n}\n\n/* Forms */\n.form-section {\n background: var(--bg-secondary);\n border: 3px solid var(--border-color);\n border-radius: 0;\n padding: 20px;\n margin-top: 24px;\n}\n\n.form-section h3 {\n font-size: 13px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n font-family: 'JetBrains Mono', monospace;\n margin-bottom: 16px;\n}\n\n.form-row {\n display: flex;\n gap: 12px;\n margin-bottom: 12px;\n}\n\ninput[type=\"text\"] {\n flex: 1;\n padding: 10px 14px;\n border: 2px solid var(--border-color);\n border-radius: 0;\n background: var(--bg-primary);\n color: var(--text-primary);\n font-size: 13px;\n font-family: 'JetBrains Mono', monospace;\n}\n\ninput[type=\"text\"]:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: none;\n}\n\ninput[type=\"text\"]::placeholder {\n color: var(--text-secondary);\n}\n\ntextarea {\n width: 100%;\n padding: 10px 14px;\n border: 2px solid var(--border-color);\n border-radius: 0;\n background: var(--bg-primary);\n color: var(--text-primary);\n font-size: 13px;\n font-family: 'JetBrains Mono', monospace;\n resize: vertical;\n min-height: 60px;\n box-sizing: border-box;\n}\ntextarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: none;\n}\ntextarea::placeholder {\n color: var(--text-secondary);\n}\n\n.priority-badge {\n display: inline-block;\n background: var(--accent);\n color: white;\n padding: 1px 6px;\n border-radius: 0;\n font-size: 10px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n margin-left: 4px;\n}\n\n.task[draggable=\"true\"] {\n cursor: grab;\n}\n.task[draggable=\"true\"]:active {\n cursor: grabbing;\n}\n.task.drag-over {\n border-top: 2px solid var(--accent);\n}\n\n/* Terminal-style log panel */\n.log-panel {\n background: #1a1a1a;\n border-radius: 0;\n margin-top: 24px;\n overflow: hidden;\n box-shadow: none;\n border: 3px solid #333;\n}\n.log-panel-header {\n padding: 10px 16px;\n background: #111;\n border-bottom: 3px solid #333;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.terminal-dots {\n display: flex;\n gap: 6px;\n align-items: center;\n}\n.terminal-dots span {\n width: 10px;\n height: 10px;\n border-radius: 0;\n display: inline-block;\n}\n.terminal-dots .dot-red { background: #DA7756; }\n.terminal-dots .dot-yellow { background: #e8b749; }\n.terminal-dots .dot-green { background: #5cb85c; }\n.terminal-title {\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n color: #6c7086;\n letter-spacing: 0.5px;\n}\n.log-feed {\n max-height: 340px;\n overflow-y: auto;\n padding: 4px 0;\n scrollbar-width: thin;\n scrollbar-color: #444 #1a1a1a;\n}\n.log-feed::-webkit-scrollbar { width: 6px; }\n.log-feed::-webkit-scrollbar-track { background: #1a1a1a; }\n.log-feed::-webkit-scrollbar-thumb { background: #444; border-radius: 0; }\n.log-entry {\n display: flex;\n gap: 8px;\n padding: 3px 16px;\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n align-items: flex-start;\n line-height: 1.5;\n transition: background 0.15s;\n}\n.log-entry:hover {\n background: rgba(69, 71, 90, 0.3);\n}\n.log-entry i {\n width: 16px;\n text-align: center;\n flex-shrink: 0;\n font-size: 11px;\n margin-top: 2px;\n}\n.log-time {\n color: #585b70;\n font-size: 11px;\n flex-shrink: 0;\n user-select: none;\n}\n.log-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: #cdd6f4;\n}\n.log-prompt {\n color: #a6e3a1;\n flex-shrink: 0;\n user-select: none;\n}\n.log-cursor {\n display: inline-block;\n width: 7px;\n height: 14px;\n background: #a6e3a1;\n animation: blink 1s step-end infinite;\n margin-left: 4px;\n vertical-align: text-bottom;\n}\n@keyframes blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n}\n.log-empty {\n padding: 20px 16px;\n color: #585b70;\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n text-align: center;\n}\n.raw-log {\n max-height: 300px;\n overflow-y: auto;\n padding: 12px 16px;\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n white-space: pre-wrap;\n word-break: break-all;\n background: #11111b;\n color: #a6adc8;\n border-top: 1px solid #313244;\n}\n.log-toggle {\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: #89b4fa;\n cursor: pointer;\n background: none;\n border: none;\n padding: 2px 8px;\n border-radius: 4px;\n transition: background 0.15s;\n}\n.log-toggle:hover {\n background: rgba(137, 180, 250, 0.1);\n}\n/* Pixel art indicator */\n.px-icon {\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 1px;\n flex-shrink: 0;\n margin-top: 5px;\n image-rendering: pixelated;\n box-shadow: 1px 0 0 0 currentColor, 0 1px 0 0 currentColor;\n}\n.log-label {\n font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.5px;\n min-width: 44px;\n flex-shrink: 0;\n text-transform: uppercase;\n}\n\nbutton {\n padding: 10px 20px;\n border: 2px solid transparent;\n border-radius: 0;\n font-size: 12px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 1px;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n transition: transform 0.1s, opacity 0.2s;\n}\n\nbutton:hover { filter: brightness(1.1); }\nbutton:active { transform: scale(0.98); }\n\n.btn-primary { background: var(--accent); color: white; border-color: var(--accent); }\n.btn-primary:hover { box-shadow: none; filter: brightness(1.15); }\n.btn-success { background: var(--success); color: white; border-color: var(--success); }\n.btn-success:hover { box-shadow: none; filter: brightness(1.15); }\n.btn-danger { background: var(--danger); color: white; border-color: var(--danger); }\n.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-color); }\n\n/* Global select styling */\nselect {\n padding: 10px 14px;\n border: 2px solid var(--border-color);\n border-radius: 0;\n background: var(--bg-primary);\n color: var(--text-primary);\n font-size: 13px;\n font-family: 'JetBrains Mono', monospace;\n cursor: pointer;\n width: 100%;\n transition: border-color 0.15s;\n -webkit-appearance: none;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 12px center;\n padding-right: 32px;\n}\nselect:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: none;\n}\ninput[type=\"number\"] {\n padding: 10px 14px;\n border: 1px solid var(--border-color);\n border-radius: 8px;\n background: var(--bg-primary);\n color: var(--text-primary);\n font-size: 14px;\n width: 100%;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\ninput[type=\"number\"]:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 3px rgba(218,119,86,0.2);\n}\ninput[type=\"password\"] {\n padding: 10px 14px;\n border: 2px solid var(--border-color);\n border-radius: 0;\n background: var(--bg-primary);\n color: var(--text-primary);\n font-size: 13px;\n font-family: 'JetBrains Mono', monospace;\n width: 100%;\n transition: border-color 0.15s;\n}\ninput[type=\"password\"]:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: none;\n}\n\n/* Styled scrollbars */\n.activity-list::-webkit-scrollbar { width: 6px; }\n.activity-list::-webkit-scrollbar-track { background: transparent; }\n.activity-list::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }\n\n/* Toast notification */\n.toast {\n position: fixed;\n bottom: 24px;\n right: 24px;\n padding: 12px 20px;\n background: var(--success);\n color: white;\n border-radius: 0;\n font-size: 12px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n box-shadow: none;\n border: 2px solid rgba(255,255,255,0.2);\n z-index: 1000;\n animation: toast-in 0.3s ease, toast-out 0.3s ease 2s forwards;\n}\n@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }\n@keyframes toast-out { from { opacity: 1; } to { opacity: 0; transform: translateY(10px); } }\n\n/* Settings-specific styles */\n.settings-label {\n font-size: 11px;\n color: var(--text-secondary);\n margin-bottom: 4px;\n display: block;\n font-family: 'JetBrains Mono', monospace;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n.settings-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n margin-top: 8px;\n}\n.settings-hint {\n color: var(--text-secondary);\n font-size: 11px;\n margin-top: 6px;\n font-family: 'JetBrains Mono', monospace;\n}\n\n/* Commit styling */\n.commit-hash {\n font-family: 'JetBrains Mono', 'SF Mono', monospace;\n font-size: 12px;\n color: var(--text-secondary);\n background: var(--bg-tertiary);\n padding: 2px 8px;\n border-radius: 0;\n border: 1px solid var(--border-color);\n}\n.commit-msg {\n font-weight: 500;\n}\n.commit-time {\n font-size: 12px;\n color: var(--text-secondary);\n}\n\n/* Control buttons */\n.controls {\n display: flex;\n gap: 12px;\n margin-top: 24px;\n}\n\n.controls form {\n flex: 1;\n}\n\n.controls button {\n width: 100%;\n padding: 14px;\n font-size: 15px;\n}\n\n/* Theme toggle */\n.theme-toggle {\n background: none;\n border: 2px solid var(--border-color);\n padding: 8px 12px;\n cursor: pointer;\n border-radius: 0;\n color: var(--text-primary);\n font-family: 'JetBrains Mono', monospace;\n}\n\n/* Session details */\n.session-card {\n background: var(--bg-secondary);\n border: 3px solid var(--border-color);\n border-radius: 0;\n padding: 20px;\n margin-bottom: 16px;\n}\n\n.session-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 16px;\n}\n\n.session-title {\n font-weight: 600;\n font-size: 16px;\n}\n\n.session-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 16px;\n}\n\n.meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.meta-label {\n font-size: 11px;\n text-transform: uppercase;\n color: var(--text-secondary);\n}\n\n.meta-value {\n font-weight: 500;\n}\n\n/* Empty state */\n.empty {\n text-align: center;\n padding: 40px;\n color: var(--text-secondary);\n}\n\n.empty i {\n font-size: 48px;\n margin-bottom: 16px;\n opacity: 0.5;\n}\n\n/* Pulsing indicator for running state */\n.pulse-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #fff;\n margin: 0 2px;\n animation: pulse-anim 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse-anim {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.4; transform: scale(0.7); }\n}\n\n/* Live timer styling */\n.live-timer {\n font-family: monospace;\n font-variant-numeric: tabular-nums;\n}\n\n@media (max-width: 1200px) {\n .cards {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 768px) {\n .layout {\n grid-template-columns: 1fr;\n }\n .sidebar {\n position: fixed;\n left: -260px;\n z-index: 100;\n transition: left 0.3s;\n width: 240px;\n }\n .sidebar.open {\n left: 0;\n }\n .hamburger {\n display: block !important;\n }\n .cards {\n grid-template-columns: 1fr;\n }\n .task-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n .log-feed {\n max-height: none;\n }\n}\n\n@media (max-width: 480px) {\n .task-grid {\n grid-template-columns: 1fr;\n }\n}\n\n.hamburger {\n display: none;\n background: none;\n border: 2px solid var(--border-color);\n padding: 8px 12px;\n cursor: pointer;\n border-radius: 0;\n color: var(--text-primary);\n font-size: 18px;\n font-family: 'JetBrains Mono', monospace;\n}\n\n/* Task detail expandable — slide transition */\n.task-detail {\n max-height: 0;\n overflow: hidden;\n padding: 0 20px 0 56px;\n border-bottom: 1px solid var(--border-color);\n transition: max-height 0.3s ease, padding 0.3s ease;\n}\n.task-detail.open {\n max-height: 400px;\n padding: 12px 20px 16px 56px;\n}\n\n.task-stages {\n display: flex;\n gap: 4px;\n margin-bottom: 12px;\n}\n\n.stage-step {\n flex: 1;\n height: 6px;\n border-radius: 3px;\n background: var(--bg-tertiary);\n}\n\n.stage-step.active {\n background: var(--warning);\n}\n\n.stage-step.done {\n background: var(--success);\n}\n\n.task-detail-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));\n gap: 8px;\n font-size: 12px;\n color: var(--text-secondary);\n}\n\n.task-detail-meta dt {\n font-weight: 600;\n color: var(--text-primary);\n}\n\n.task-criteria {\n margin-top: 8px;\n padding: 8px 12px;\n background: var(--bg-tertiary);\n border-radius: 8px;\n font-size: 12px;\n}\n\n.task-clickable {\n cursor: pointer;\n}\n\n/* Task Detail Page */\n.back-link {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--text-secondary);\n text-decoration: none;\n font-size: 13px;\n margin-bottom: 16px;\n transition: color 0.2s;\n}\n.back-link:hover { color: var(--accent); }\n\n.task-detail-header {\n display: flex;\n align-items: center;\n gap: 16px;\n margin-bottom: 24px;\n}\n.task-detail-header h1 {\n font-size: 22px;\n font-weight: 600;\n flex: 1;\n}\n.td-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 12px;\n border-radius: 16px;\n font-size: 12px;\n font-weight: 500;\n}\n.td-badge.pending { background: var(--bg-tertiary); color: var(--text-secondary); }\n.td-badge.in_progress { background: var(--warning); color: #fff; }\n.td-badge.done { background: var(--success); color: #fff; }\n.td-badge.blocked { background: var(--danger); color: #fff; }\n\n.td-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 16px;\n margin-bottom: 24px;\n}\n@media (max-width: 768px) { .td-grid { grid-template-columns: 1fr; } }\n\n.td-info {\n background: var(--bg-secondary);\n border: 1px solid var(--border-color);\n border-radius: 12px;\n padding: 20px;\n}\n.td-info-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--text-secondary);\n margin-bottom: 4px;\n}\n.td-info-value {\n font-size: 14px;\n margin-bottom: 14px;\n}\n.td-info-value:last-child { margin-bottom: 0; }\n\n.td-metrics {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n}\n.td-metric {\n background: var(--bg-secondary);\n border: 1px solid var(--border-color);\n border-radius: 12px;\n padding: 16px;\n text-align: center;\n}\n.td-metric-val {\n font-size: 28px;\n font-weight: 600;\n color: var(--accent);\n}\n.td-metric-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--text-secondary);\n margin-top: 4px;\n}\n\n.td-log-section {\n background: var(--bg-secondary);\n border: 1px solid var(--border-color);\n border-radius: 12px;\n margin-bottom: 24px;\n overflow: hidden;\n}\n.td-log-header {\n padding: 14px 20px;\n border-bottom: 1px solid var(--border-color);\n font-weight: 600;\n font-size: 14px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.td-log-body {\n max-height: 400px;\n overflow-y: auto;\n padding: 8px 0;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n line-height: 1.7;\n}\n.td-log-entry {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 3px 16px;\n}\n.td-log-entry:hover { background: var(--bg-tertiary); }\n.td-log-time {\n color: var(--text-secondary);\n flex-shrink: 0;\n font-size: 11px;\n}\n.td-log-icon {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 5px;\n}\n.td-log-label {\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.5px;\n flex-shrink: 0;\n min-width: 44px;\n}\n.td-log-text {\n flex: 1;\n word-break: break-all;\n color: var(--text-primary);\n}\n.td-log-empty {\n padding: 32px;\n text-align: center;\n color: var(--text-secondary);\n}\n\n.td-sessions {\n background: var(--bg-secondary);\n border: 1px solid var(--border-color);\n border-radius: 12px;\n overflow: hidden;\n}\n.td-sessions-header {\n padding: 14px 20px;\n border-bottom: 1px solid var(--border-color);\n font-weight: 600;\n font-size: 14px;\n}\n.td-session-item {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 12px 20px;\n border-bottom: 1px solid var(--border-color);\n font-size: 13px;\n}\n.td-session-item:last-child { border-bottom: none; }\n.td-session-num {\n font-weight: 600;\n color: var(--accent);\n min-width: 70px;\n}\n.td-session-meta {\n display: flex;\n gap: 16px;\n flex: 1;\n color: var(--text-secondary);\n font-size: 12px;\n}\n'''\n\nHTML_TEMPLATE = Template('''<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"$theme\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>PocketCoder-A1 Dashboard</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap\" media=\"print\" onload=\"this.media='all'\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css\" media=\"print\" onload=\"this.media='all'\">\n <style>''' + CSS + '''</style>\n</head>\n<body>\n <div class=\"layout\">\n <aside class=\"sidebar\">\n <div class=\"logo-ascii\"><pre class=\"logo-art\"> &#9618;&#9608;&#9608;&#9608;&#9608;&#9608;&#9618; &#9617;&#9612;\n &#9608;&#9619; &#9619;&#9608; &#9617;&#9608;&#9612;\n &#9608;&#9608;&#9608;&#9608;&#9608;&#9608;&#9608; &#9617;&#9608;&#9612;\n &#9608;&#9619; &#9619;&#9608; &#9608;&#9612;\n &#9608;&#9612; &#9612;&#9608; &#9608;&#9608;&#9608;</pre></div>\n <div class=\"logo-sub\">pocketcoder // v0.2.4</div>\n\n <nav>\n <div class=\"nav-section\">\n <div class=\"nav-title\">Overview</div>\n <a href=\"/\" class=\"nav-item $nav_dashboard\">\n <i class=\"bi bi-speedometer2\"></i> Dashboard\n </a>\n <a href=\"/tasks\" class=\"nav-item $nav_tasks\">\n <i class=\"bi bi-list-check\"></i> Tasks\n </a>\n <a href=\"/sessions\" class=\"nav-item $nav_sessions\">\n <i class=\"bi bi-terminal\"></i> Sessions\n </a>\n </div>\n\n <div class=\"nav-section\">\n <div class=\"nav-title\">Activity</div>\n <a href=\"/log\" class=\"nav-item $nav_log\">\n <i class=\"bi bi-journal-text\"></i> Activity Log\n </a>\n <a href=\"/commits\" class=\"nav-item $nav_commits\">\n <i class=\"bi bi-git\"></i> Commits\n </a>\n <!-- Transform moved into Tasks page -->\n </div>\n\n <div class=\"nav-section\">\n <div class=\"nav-title\">Settings</div>\n <a href=\"/settings\" class=\"nav-item $nav_settings\">\n <i class=\"bi bi-gear\"></i> Settings\n </a>\n </div>\n </nav>\n </aside>\n\n <main class=\"main\">\n <button class=\"hamburger\" onclick=\"document.querySelector('.sidebar').classList.toggle('open')\">\n <i class=\"bi bi-list\"></i>\n </button>\n $content\n </main>\n </div>\n\n <script>\n // Theme toggle\n function toggleTheme() {\n const html = document.documentElement;\n const current = html.getAttribute('data-theme');\n const next = current === 'dark' ? 'light' : 'dark';\n html.setAttribute('data-theme', next);\n localStorage.setItem('theme', next);\n }\n\n // Load saved theme\n const saved = localStorage.getItem('theme');\n if (saved) {\n document.documentElement.setAttribute('data-theme', saved);\n }\n\n // Escape HTML\n function escHtml(s) {\n const d = document.createElement('div');\n d.textContent = s;\n return d.innerHTML;\n }\n\n // --- AJAX polling (dashboard page only) ---\n const pageName = '$page_name';\n let logIndex = 0;\n\n function fmtTokens(n) {\n if (!n || n === 0) return '0';\n if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';\n if (n >= 1000) return (n / 1000).toFixed(1) + 'K';\n return n.toString();\n }\n\n function fmtDuration(s) {\n if (!s || s <= 0) return '0s';\n if (s < 60) return s + 's';\n const m = Math.floor(s / 60);\n const sec = s % 60;\n return m + 'm ' + sec + 's';\n }\n\n function estimateCost(tokensIn, tokensOut, cacheRead) {\n // Claude Opus pricing (approximate): 15/M input, 75/M output, 1.5/M cache read\n const costIn = (tokensIn - (cacheRead || 0)) * 15 / 1000000;\n const costCache = (cacheRead || 0) * 1.5 / 1000000;\n const costOut = tokensOut * 75 / 1000000;\n return Math.max(0, costIn + costCache + costOut);\n }\n\n function updateStatus() {\n fetch('/api/status')\n .then(r => r.json())\n .then(data => {\n const badge = document.getElementById('status-badge');\n if (badge) {\n if (data.running) {\n badge.className = 'status status-running';\n badge.innerHTML = '<i class=\"bi bi-play-circle-fill\"></i> <span class=\"pulse-dot\"></span> Running';\n } else if (data.checkpoint && data.checkpoint.status === 'COMPLETED') {\n badge.className = 'status status-completed';\n badge.innerHTML = '<i class=\"bi bi-check-circle-fill\"></i> Completed';\n } else {\n badge.className = 'status status-stopped';\n badge.innerHTML = '<i class=\"bi bi-stop-circle-fill\"></i> Stopped';\n }\n }\n const tc = document.getElementById('task-count');\n if (tc && data.progress) {\n let txt = data.progress[0] + '/' + data.progress[1];\n tc.textContent = txt;\n const sub = tc.parentElement && tc.parentElement.querySelector('.card-sub');\n if (sub) {\n sub.textContent = data.progress[2] ? 'completed, ' + data.progress[2] + ' blocked' : 'completed';\n }\n }\n const sc = document.getElementById('session-count');\n if (sc && data.checkpoint) {\n sc.textContent = '#' + data.checkpoint.session;\n }\n const fc = document.getElementById('files-count');\n if (fc && data.checkpoint) {\n fc.textContent = (data.checkpoint.files_modified || []).length;\n }\n // Token metrics\n const m = data.metrics || {};\n const tokC = document.getElementById('tokens-count');\n if (tokC) {\n tokC.textContent = fmtTokens(m.tokens_in || 0) + ' / ' + fmtTokens(m.tokens_out || 0);\n }\n const tokSub = document.getElementById('tokens-sub');\n if (tokSub && m.cache_read) {\n tokSub.textContent = 'cache: ' + fmtTokens(m.cache_read);\n }\n const tokBar = document.getElementById('tokens-bar');\n if (tokBar) {\n const pct = Math.min(100, (m.context_percent || 0) * 100);\n tokBar.style.width = pct + '%';\n // Change color at threshold\n if (pct >= 70) tokBar.style.background = '#ef4444';\n else if (pct >= 50) tokBar.style.background = '#f59e0b';\n else tokBar.style.background = 'var(--accent)';\n }\n // Context percent text\n const ctxPct = document.getElementById('context-pct');\n if (ctxPct) {\n const pct = Math.round((m.context_percent || 0) * 100);\n ctxPct.textContent = pct + '%';\n }\n // Cost\n const costEl = document.getElementById('cost-value');\n if (costEl) {\n const cost = estimateCost(m.tokens_in || 0, m.tokens_out || 0, m.cache_read || 0);\n costEl.textContent = '$$' + cost.toFixed(3);\n }\n // Duration\n const durEl = document.getElementById('duration-value');\n if (durEl) {\n durEl.textContent = fmtDuration(m.session_duration || 0);\n }\n // Queue message section\n const qms = document.getElementById('queue-msg-section');\n if (qms) {\n qms.style.display = data.running ? 'block' : 'none';\n }\n })\n .catch(() => {});\n }\n\n function updateLog() {\n fetch('/api/log?since=' + logIndex)\n .then(r => r.json())\n .then(data => {\n if (data.entries && data.entries.length > 0) {\n const feed = document.getElementById('action-feed');\n const rawlog = document.getElementById('raw-log');\n if (feed && rawlog) {\n data.entries.forEach(e => {\n const labelMap = {read:'READ', edit:'EDIT', write:'WRITE', bash:'BASH', thinking:'THINK', text:'OUT', metric:'METRIC', verify:'CHECK'};\n const colorMap = {read:'#89b4fa', edit:'#fab387', write:'#a6e3a1', bash:'#cba6f7', thinking:'#f9e2af', text:'#6c7086', metric:'#89dceb', verify:'#a6e3a1'};\n const label = labelMap[e.type] || 'LOG';\n const color = colorMap[e.type] || '#6c7086';\n const pixelIcon = '<span class=\"px-icon\" style=\"background:' + color + '\"></span>';\n const tidTag = e.task_id ? '<span style=\"color:var(--accent);font-size:10px;margin-right:4px\">[' + escHtml(e.task_id) + ']</span>' : '';\n feed.innerHTML += '<div class=\"log-entry\">' + pixelIcon + '<span class=\"log-time\">' + e.time + '</span><span class=\"log-label\" style=\"color:' + color + '\">' + label + '</span>' + tidTag + '<span class=\"log-text\">' + escHtml(e.line.substring(0,150)) + '</span></div>';\n rawlog.textContent += e.line + '\\\\n';\n });\n feed.scrollTop = feed.scrollHeight;\n rawlog.scrollTop = rawlog.scrollHeight;\n }\n logIndex = data.total;\n }\n })\n .catch(() => {});\n }\n\n function sendQueueMessage(e) {\n e.preventDefault();\n const input = document.getElementById('queue-msg-input');\n const status = document.getElementById('queue-msg-status');\n if (!input.value.trim()) return;\n fetch('/queue-message', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'message=' + encodeURIComponent(input.value)\n }).then(r => r.json()).then(data => {\n status.textContent = 'Message queued at ' + new Date().toLocaleTimeString();\n input.value = '';\n }).catch(() => { status.textContent = 'Failed to send'; });\n }\n\n // Live timer tick (updates every second when running)\n let sessionStartTime = 0;\n let agentRunning = false;\n\n function tickTimer() {\n if (!agentRunning || !sessionStartTime) return;\n const elapsed = Math.floor(Date.now() / 1000 - sessionStartTime);\n const durEl = document.getElementById('duration-value');\n if (durEl) durEl.textContent = fmtDuration(elapsed);\n }\n\n // Toggle task detail on tasks page\n function toggleTaskDetail(taskId) {\n const el = document.getElementById('detail-' + taskId);\n if (el) el.classList.toggle('open');\n }\n\n if (pageName === 'dashboard') {\n // Wrap updateStatus to also track timer state\n const _origUpdateStatus = updateStatus;\n updateStatus = function() {\n fetch('/api/status')\n .then(r => r.json())\n .then(data => {\n agentRunning = data.running;\n if (data.metrics && data.metrics.session_start) {\n sessionStartTime = data.metrics.session_start;\n }\n }).catch(() => {});\n _origUpdateStatus();\n };\n setInterval(updateStatus, 3000);\n setInterval(updateLog, 2000);\n setInterval(tickTimer, 1000);\n updateStatus();\n } else if (pageName === 'tasks') {\n // No auto-reload on tasks page (drag-drop needs stable DOM)\n } else if (pageName === 'task_detail') {\n // Task detail has its own AJAX polling (injected by build_task_detail_page)\n } else {\n setTimeout(() => location.reload(), 5000);\n }\n\n // --- Drag and Drop (tasks page) ---\n if (pageName === 'tasks') {\n let dragSrc = null;\n document.querySelectorAll('.task[draggable]').forEach(el => {\n el.addEventListener('dragstart', e => {\n dragSrc = el;\n el.style.opacity = '0.4';\n e.dataTransfer.effectAllowed = 'move';\n });\n el.addEventListener('dragover', e => {\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n el.classList.add('drag-over');\n });\n el.addEventListener('dragleave', () => {\n el.classList.remove('drag-over');\n });\n el.addEventListener('drop', e => {\n e.preventDefault();\n el.classList.remove('drag-over');\n if (dragSrc && dragSrc !== el) {\n el.parentNode.insertBefore(dragSrc, el);\n const order = [...document.querySelectorAll('.task[data-task-id]')]\n .map(t => t.dataset.taskId);\n fetch('/api/reorder', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({order})\n }).then(() => location.reload());\n }\n });\n el.addEventListener('dragend', () => {\n el.style.opacity = '';\n });\n });\n }\n </script>\n</body>\n</html>\n''')\n\n\nclass DashboardHandler(BaseHTTPRequestHandler):\n def log_message(self, format, *args):\n pass\n\n def _safe_write(self, data):\n try:\n self.wfile.write(data)\n except BrokenPipeError:\n pass\n\n def do_GET(self):\n path = urlparse(self.path).path\n\n if path == '/' or path == '/index.html':\n self.send_page('dashboard')\n elif path == '/tasks':\n self.send_page('tasks')\n elif path == '/sessions':\n self.send_page('sessions')\n elif path == '/log':\n self.send_page('log')\n elif path == '/commits':\n self.send_page('commits')\n elif path == '/settings':\n self.send_page('settings')\n elif path == '/transform':\n self.send_response(302)\n self.send_header('Location', '/tasks')\n self.end_headers()\n elif path == '/api/status':\n self.send_json_status()\n elif path.startswith('/api/log'):\n self.send_json_log()\n elif path == '/api/config':\n self.send_json_config()\n elif re.match(r'^/api/task/(task_\\d+)$', path):\n task_id = re.match(r'^/api/task/(task_\\d+)$', path).group(1)\n self.send_json_task(task_id)\n elif re.match(r'^/task/(task_\\d+)$', path):\n task_id = re.match(r'^/task/(task_\\d+)$', path).group(1)\n self.send_page('task_detail', task_id=task_id)\n else:\n self.send_error(404)\n\n def do_POST(self):\n content_length = int(self.headers.get('Content-Length', 0))\n post_data = self.rfile.read(content_length).decode('utf-8')\n params = parse_qs(post_data)\n\n if self.path == '/add-task':\n task_text = params.get('task', [''])[0]\n task_desc = params.get('description', [''])[0]\n if task_text:\n tasks = TaskManager(PROJECT_DIR)\n tasks.add_task(task_text, description=task_desc)\n log_activity(\"Task added\", task_text, \"success\")\n self.redirect('/')\n\n elif self.path == '/add-thought':\n thought = params.get('thought', [''])[0]\n if thought:\n tasks = TaskManager(PROJECT_DIR)\n tasks.add_raw_thought(thought)\n log_activity(\"Thought added\", thought, \"info\")\n self.redirect('/')\n\n elif self.path == '/start':\n self.start_agent()\n self.redirect('/')\n\n elif re.match(r'^/start-task/(task_\\d+)$', self.path):\n task_id = re.match(r'^/start-task/(task_\\d+)$', self.path).group(1)\n self.start_task_agent(task_id)\n\n elif self.path == '/stop':\n self.stop_agent()\n self.redirect('/')\n\n elif re.match(r'^/delete-task/(task_\\d+)$', self.path):\n task_id = re.match(r'^/delete-task/(task_\\d+)$', self.path).group(1)\n tasks_mgr = TaskManager(PROJECT_DIR)\n tasks_mgr.delete_task(task_id)\n log_activity(\"Task deleted\", task_id, \"warning\")\n self.redirect('/tasks')\n\n elif self.path == '/add-tasks-bulk':\n lines_raw = params.get('tasks_bulk', [''])[0]\n if lines_raw.strip():\n tasks_mgr = TaskManager(PROJECT_DIR)\n count = 0\n for line in lines_raw.strip().split('\\n'):\n line = line.strip().lstrip('- ').lstrip('* ').lstrip('0123456789.)')\n line = line.strip()\n if line:\n tasks_mgr.add_task(line)\n count += 1\n log_activity(\"Bulk tasks added\", f\"{count} tasks\", \"success\")\n self.redirect('/tasks')\n\n elif self.path == '/toggle-theme':\n self.redirect('/')\n elif self.path == '/transform':\n raw_text = params.get('text', [''])[0]\n if raw_text:\n result = self._transform_text(raw_text)\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps(result).encode('utf-8'))\n else:\n self.send_response(400)\n self.end_headers()\n\n elif self.path == '/transform-confirm':\n try:\n body = json.loads(post_data)\n tasks_list = body.get('tasks', [])\n tasks_mgr = TaskManager(PROJECT_DIR)\n added = 0\n for t in tasks_list:\n if t.get('title'):\n tasks_mgr.add_task(t['title'], description=t.get('description', ''), success_criteria=t.get('success_criteria'))\n added += 1\n log_activity(\"Transform confirmed\", f\"{added} tasks added\", \"success\")\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps({\"ok\": True, \"added\": added}).encode('utf-8'))\n except Exception:\n self.send_response(400)\n self.end_headers()\n\n elif self.path == '/queue-message':\n msg_text = params.get('message', [''])[0]\n if msg_text:\n queue_file = PROJECT_DIR / '.a1' / 'queue.json'\n queue_data = {\"messages\": []}\n if queue_file.exists():\n try:\n queue_data = json.loads(queue_file.read_text())\n except (json.JSONDecodeError, IOError):\n pass\n queue_data[\"messages\"].append({\n \"text\": msg_text,\n \"added_at\": datetime.now().isoformat(),\n \"read\": False,\n })\n queue_file.write_text(json.dumps(queue_data, indent=2, ensure_ascii=False))\n log_activity(\"Message queued\", msg_text[:50], \"info\")\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(b'{\"ok\": true}')\n\n elif self.path == '/api/config':\n try:\n body = json.loads(post_data)\n from .config import Config\n config = Config(PROJECT_DIR)\n for key, value in body.items():\n config.set(key, value)\n log_activity(\"Config updated\", \", \".join(body.keys()), \"info\")\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps({\"ok\": True}).encode('utf-8'))\n except Exception as e:\n self.send_response(400)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps({\"error\": str(e)}).encode('utf-8'))\n\n elif self.path == '/api/reorder':\n try:\n body = json.loads(post_data)\n task_ids = body.get('order', [])\n if task_ids:\n tasks_mgr = TaskManager(PROJECT_DIR)\n tasks_mgr.reorder_tasks(task_ids)\n log_activity(\"Tasks reordered\", f\"{len(task_ids)} tasks\", \"info\")\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(b'{\"ok\": true}')\n except Exception:\n self.send_response(400)\n self.end_headers()\n else:\n self.send_error(404)\n\n def redirect(self, location):\n self.send_response(302)\n self.send_header('Location', location)\n self.end_headers()\n\n def send_page(self, page, task_id=None):\n theme = 'light' # Default light theme\n content = self.build_content(page, task_id=task_id)\n\n nav_active = {\n 'nav_dashboard': 'active' if page == 'dashboard' else '',\n 'nav_tasks': 'active' if page in ('tasks', 'task_detail') else '',\n 'nav_sessions': 'active' if page == 'sessions' else '',\n 'nav_log': 'active' if page == 'log' else '',\n 'nav_commits': 'active' if page == 'commits' else '',\n 'nav_transform': 'active' if page == 'transform' else '',\n 'nav_settings': 'active' if page == 'settings' else '',\n }\n\n html = HTML_TEMPLATE.substitute(\n theme=theme,\n content=content,\n page_name=page,\n **nav_active\n )\n\n self.send_response(200)\n self.send_header('Content-Type', 'text/html; charset=utf-8')\n self.end_headers()\n try:\n self.wfile.write(html.encode('utf-8'))\n except (BrokenPipeError, ConnectionResetError):\n pass\n\n def build_content(self, page, task_id=None):\n if page == 'dashboard':\n return self.build_dashboard()\n elif page == 'tasks':\n return self.build_tasks_page()\n elif page == 'task_detail' and task_id:\n return self.build_task_detail_page(task_id)\n elif page == 'sessions':\n return self.build_sessions_page()\n elif page == 'log':\n return self.build_log_page()\n elif page == 'commits':\n return self.build_commits_page()\n elif page == 'settings':\n return self.build_settings_page()\n elif page == 'transform':\n return self.build_transform_page()\n return ''\n\n def build_dashboard(self):\n checkpoint = CheckpointManager(PROJECT_DIR)\n tasks_mgr = TaskManager(PROJECT_DIR)\n\n cp = checkpoint.load()\n all_tasks = tasks_mgr.get_tasks()\n done, total, blocked = tasks_mgr.get_progress()\n progress = int((done / total * 100)) if total > 0 else 0\n\n # Status\n if AGENT_RUNNING:\n status_class = 'status-running'\n status_text = '<i class=\"bi bi-play-circle-fill\"></i> Running'\n elif cp.get('status') == 'COMPLETED':\n status_class = 'status-completed'\n status_text = '<i class=\"bi bi-check-circle-fill\"></i> Completed'\n else:\n status_class = 'status-stopped'\n status_text = '<i class=\"bi bi-stop-circle-fill\"></i> Stopped'\n\n # Tasks HTML (last 5, sorted by priority)\n sorted_tasks = sorted(all_tasks, key=lambda t: (t.status == 'done', t.priority))\n tasks_html = ''\n for t in sorted_tasks[:5]:\n if t.status == 'done':\n check_class = 'done'\n check_icon = '<i class=\"bi bi-check\"></i>'\n elif t.status == 'in_progress':\n check_class = 'progress'\n check_icon = '<i class=\"bi bi-arrow-repeat\"></i>'\n elif t.status == 'blocked':\n check_class = 'blocked'\n check_icon = '<i class=\"bi bi-x-circle\" style=\"color:var(--danger)\"></i>'\n else:\n check_class = ''\n check_icon = ''\n\n pri = getattr(t, 'priority', 0)\n pri_badge = f'<span class=\"priority-badge\">#{pri}</span>' if pri and t.status not in ('done', 'blocked') else ''\n\n tasks_html += f'''\n <a href=\"/task/{t.id}\" class=\"task task-clickable\" style=\"text-decoration:none;color:inherit\">\n <div class=\"task-check {check_class}\">{check_icon}</div>\n <div class=\"task-content\">\n <div class=\"task-title\">{esc(t.title)}</div>\n <div class=\"task-meta\">{esc(t.id)} {pri_badge}</div>\n </div>\n <i class=\"bi bi-arrow-right\" style=\"color:var(--text-secondary);font-size:12px\"></i>\n </a>\n '''\n\n if not tasks_html:\n tasks_html = '<div class=\"empty\"><i class=\"bi bi-inbox\"></i><p>No tasks yet</p></div>'\n\n # Activity HTML (last 5) with status icons\n status_icons = {\n 'success': '<i class=\"bi bi-check-circle-fill\" style=\"color:var(--success)\"></i>',\n 'error': '<i class=\"bi bi-x-circle-fill\" style=\"color:var(--danger)\"></i>',\n 'warning': '<i class=\"bi bi-exclamation-triangle-fill\" style=\"color:var(--warning)\"></i>',\n 'info': '<i class=\"bi bi-info-circle-fill\" style=\"color:var(--accent)\"></i>',\n }\n activity_html = ''\n for a in reversed(ACTIVITY_LOG[-5:]):\n icon = status_icons.get(a['status'], status_icons['info'])\n activity_html += f'''\n <div class=\"activity-item\">\n <span class=\"activity-time\">{esc(a['time'])}</span>\n <span style=\"flex-shrink:0\">{icon}</span>\n <span class=\"activity-text\"><strong>{esc(a['action'])}</strong> <span class=\"activity-details\">{esc(a['details'])}</span></span>\n </div>\n '''\n\n if not activity_html:\n activity_html = '<div class=\"empty\" style=\"padding:20px\"><i class=\"bi bi-clock-history\"></i><p>No activity yet</p></div>'\n\n # Control buttons\n if AGENT_RUNNING:\n control_html = '''\n <form method=\"POST\" action=\"/stop\">\n <button type=\"submit\" class=\"btn-danger\"><i class=\"bi bi-stop-fill\"></i> Stop Agent</button>\n </form>\n '''\n else:\n control_html = '''\n <form method=\"POST\" action=\"/start\">\n <button type=\"submit\" class=\"btn-success\"><i class=\"bi bi-play-fill\"></i> Start Agent</button>\n </form>\n '''\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Dashboard</h1>\n <div class=\"header-actions\">\n <span class=\"status {status_class}\" id=\"status-badge\">{status_text}</span>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n </div>\n\n <div class=\"cards\">\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-check2-square\"></i> Tasks</div>\n <div class=\"card-value\" id=\"task-count\">{done}/{total}</div>\n <div class=\"card-sub\">{'completed' + (f', {blocked} blocked' if blocked else '')}</div>\n <div class=\"progress\">\n <div class=\"progress-fill\" style=\"width: {progress}%\"></div>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-terminal\"></i> Session</div>\n <div class=\"card-value\" id=\"session-count\">#{cp.get('session', 0)}</div>\n <div class=\"card-sub\">{cp.get('status', 'Not started')}</div>\n </div>\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-lightning-charge\" style=\"color:#f59e0b\"></i> Tokens</div>\n <div class=\"card-value\" id=\"tokens-count\" style=\"font-size:22px\">0 / 0</div>\n <div class=\"card-sub\" id=\"tokens-sub\">in / out</div>\n <div class=\"progress\">\n <div class=\"progress-fill\" id=\"tokens-bar\" style=\"width: 0%\"></div>\n </div>\n <div class=\"card-sub\" style=\"margin-top:4px\">Context: <strong id=\"context-pct\">0%</strong> <span style=\"color:var(--muted);font-size:11px\">(auto-save at 70%)</span></div>\n </div>\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-currency-dollar\" style=\"color:#10b981\"></i> Cost</div>\n <div class=\"card-value\" id=\"cost-value\" style=\"font-size:22px\">$$0.00</div>\n <div class=\"card-sub\">this session</div>\n </div>\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-stopwatch\" style=\"color:#6366f1\"></i> Duration</div>\n <div class=\"card-value\" id=\"duration-value\">0s</div>\n <div class=\"card-sub\" id=\"duration-sub\">current session</div>\n </div>\n <div class=\"card\">\n <div class=\"card-title\"><i class=\"bi bi-file-earmark-code\"></i> Files</div>\n <div class=\"card-value\" id=\"files-count\">{len(cp.get('files_modified', []))}</div>\n <div class=\"card-sub\">modified</div>\n </div>\n </div>\n\n <div class=\"task-list\">\n <div class=\"task-header\">\n <h3>Recent Tasks</h3>\n <a href=\"/tasks\" class=\"btn-secondary\" style=\"text-decoration:none\">View All</a>\n </div>\n {tasks_html}\n </div>\n\n <div class=\"form-section\">\n <h3>Quick Add</h3>\n <form method=\"POST\" action=\"/add-task\">\n <div class=\"form-row\">\n <input type=\"text\" name=\"task\" placeholder=\"Task title...\" required>\n <button type=\"submit\" class=\"btn-primary\"><i class=\"bi bi-plus\"></i> Add</button>\n </div>\n <textarea name=\"description\" placeholder=\"Description (optional)...\" rows=\"2\"></textarea>\n </form>\n </div>\n\n <div class=\"controls\">\n {control_html}\n </div>\n\n <div class=\"form-section\" id=\"queue-msg-section\" style=\"{'display:block' if AGENT_RUNNING else 'display:none'}\">\n <h3><i class=\"bi bi-envelope\"></i> Message to Agent</h3>\n <form onsubmit=\"sendQueueMessage(event)\">\n <div class=\"form-row\">\n <input type=\"text\" id=\"queue-msg-input\" placeholder=\"Send instruction to agent (next session)...\" required>\n <button type=\"submit\" class=\"btn-primary\"><i class=\"bi bi-send\"></i> Send</button>\n </div>\n </form>\n <div id=\"queue-msg-status\" style=\"font-size:12px;color:var(--text-secondary);margin-top:6px\"></div>\n </div>\n\n <div class=\"log-panel\">\n <div class=\"log-panel-header\">\n <div style=\"display:flex;align-items:center;gap:12px\">\n <div class=\"terminal-dots\">\n <span class=\"dot-red\"></span>\n <span class=\"dot-yellow\"></span>\n <span class=\"dot-green\"></span>\n </div>\n <span class=\"terminal-title\">agent@pocketcoder ~ live-log</span>\n </div>\n <button class=\"log-toggle\" onclick=\"document.getElementById('raw-log-wrap').style.display = document.getElementById('raw-log-wrap').style.display === 'none' ? 'block' : 'none'\">raw</button>\n </div>\n <div class=\"log-feed\" id=\"action-feed\">\n {self._render_log_entries()}\n </div>\n <div id=\"raw-log-wrap\" style=\"display:none\">\n <div class=\"raw-log\" id=\"raw-log\">{self._render_raw_log()}</div>\n </div>\n </div>\n\n <div class=\"activity\">\n <div class=\"activity-header\">Recent Activity</div>\n <div class=\"activity-list\">\n {activity_html}\n </div>\n </div>\n '''\n\n def build_tasks_page(self):\n tasks_mgr = TaskManager(PROJECT_DIR)\n all_tasks = tasks_mgr.get_tasks()\n all_tasks.sort(key=lambda t: (t.status == 'done', t.priority))\n thoughts = tasks_mgr.get_raw_thoughts()\n\n tasks_html = ''\n for t in all_tasks:\n if t.status == 'done':\n check_class = 'done'\n check_icon = '<i class=\"bi bi-check\"></i>'\n elif t.status == 'in_progress':\n check_class = 'progress'\n check_icon = '<i class=\"bi bi-arrow-repeat\"></i>'\n elif t.status == 'blocked':\n check_class = 'blocked'\n check_icon = '<i class=\"bi bi-x-circle\" style=\"color:var(--danger)\"></i>'\n else:\n check_class = ''\n check_icon = ''\n\n desc = t.description[:80] if t.description else ''\n pri = getattr(t, 'priority', 0)\n pri_badge = f'<span class=\"priority-badge\">#{pri}</span>' if pri and t.status not in ('done', 'blocked') else ''\n phase_text = getattr(t, 'phase', '') or ''\n phase_tag = f'<span class=\"task-phase\">{esc(phase_text)}</span>' if phase_text else ''\n status_class = t.status.replace(' ', '_')\n\n tasks_html += f'''\n <a href=\"/task/{t.id}\" class=\"task task-clickable\" data-task-id=\"{t.id}\" style=\"text-decoration:none;color:inherit\">\n <div class=\"task-status-stripe {status_class}\"></div>\n <div class=\"task-top-row\">\n <div class=\"task-check {check_class}\">{check_icon}</div>\n <div class=\"task-meta\">{esc(t.id)} {pri_badge}</div>\n {phase_tag}\n </div>\n <div class=\"task-content\">\n <div class=\"task-title\">{esc(t.title)}</div>\n {f'<div class=\"task-desc-preview\">{esc(desc)}</div>' if desc else ''}\n </div>\n <div class=\"task-card-footer\">\n <span>{esc(t.status.replace(\"_\", \" \").upper())}</span>\n <i class=\"bi bi-arrow-right\"></i>\n </div>\n </a>\n '''\n\n thoughts_html = ''\n if thoughts:\n for th in thoughts:\n thoughts_html += f'''\n <div class=\"task\">\n <div class=\"task-check\"><i class=\"bi bi-lightbulb\"></i></div>\n <div class=\"task-content\">\n <div class=\"task-title\">{esc(th['text'])}</div>\n <div class=\"task-meta\">Raw thought</div>\n </div>\n </div>\n '''\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Tasks</h1>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n\n <div class=\"task-list\">\n <div class=\"task-header\">\n <h3>[ {len(all_tasks)} tasks ]</h3>\n </div>\n <div class=\"task-grid\">\n {tasks_html if tasks_html else '<div class=\"empty\" style=\"grid-column:1/-1\"><i class=\"bi bi-inbox\"></i><p>No tasks yet</p></div>'}\n </div>\n </div>\n\n {('<div class=\"task-list\" style=\"margin-top:24px\"><div class=\"task-header\"><h3>Raw Thoughts</h3></div>' + thoughts_html + '</div>') if thoughts_html else ''}\n\n <div class=\"form-section\">\n <h3>// Add Task</h3>\n <form method=\"POST\" action=\"/add-task\">\n <div class=\"form-row\">\n <input type=\"text\" name=\"task\" placeholder=\"$ pca task add &quot;title&quot;\" required>\n <button type=\"submit\" class=\"btn-primary\"><i class=\"bi bi-plus\"></i> Add</button>\n </div>\n <textarea name=\"description\" placeholder=\"description (optional)...\" rows=\"3\"></textarea>\n </form>\n <form method=\"POST\" action=\"/add-thought\" style=\"margin-top:12px\">\n <div class=\"form-row\">\n <input type=\"text\" name=\"thought\" placeholder=\"$ pca think &quot;idea&quot;\">\n <button type=\"submit\" class=\"btn-secondary\"><i class=\"bi bi-lightbulb\"></i> Think</button>\n </div>\n </form>\n </div>\n\n <div class=\"form-section\">\n <h3>// Bulk Import</h3>\n <p style=\"color:var(--text-secondary);font-size:11px;margin-bottom:12px;font-family:'JetBrains Mono',monospace\">One task per line. Priorities auto-assigned.</p>\n <form method=\"POST\" action=\"/add-tasks-bulk\">\n <textarea name=\"tasks_bulk\" rows=\"4\" placeholder=\"Add login page&#10;Write unit tests&#10;Fix responsive layout&#10;Deploy to staging\"></textarea>\n <div style=\"margin-top:12px\">\n <button type=\"submit\" class=\"btn-primary\"><i class=\"bi bi-list-task\"></i> Import All</button>\n </div>\n </form>\n </div>\n\n <div class=\"form-section\">\n <h3>// AI Transform</h3>\n <p style=\"color:var(--text-secondary);font-size:11px;margin-bottom:12px;font-family:'JetBrains Mono',monospace\">\n Write raw text, notes, ideas — AI breaks them into structured tasks.\n </p>\n <textarea id=\"transform-input\" rows=\"5\" placeholder=\"Example:&#10;I need a login page with email and password, then registration with validation,&#10;password reset via email, and unit tests for the whole auth module\"></textarea>\n <div style=\"margin-top:12px;display:flex;align-items:center;gap:12px\">\n <button class=\"btn-primary\" onclick=\"doTransform()\" id=\"transform-btn\">\n <i class=\"bi bi-magic\"></i> Transform\n </button>\n <span id=\"transform-status\" style=\"font-size:11px;color:var(--text-secondary);font-family:'JetBrains Mono',monospace\"></span>\n </div>\n </div>\n\n <div id=\"transform-preview\" style=\"display:none;margin-top:24px\">\n <div class=\"task-list\">\n <div class=\"task-header\">\n <h3>[ preview ]</h3>\n <button class=\"btn-success\" onclick=\"confirmTransform()\">\n <i class=\"bi bi-check-all\"></i> Add Selected\n </button>\n </div>\n <div id=\"preview-tasks\" class=\"task-grid\"></div>\n </div>\n </div>\n\n <script>\n let transformedTasks = [];\n\n function doTransform() {{\n const text = document.getElementById('transform-input').value.trim();\n if (!text) return;\n const btn = document.getElementById('transform-btn');\n const status = document.getElementById('transform-status');\n btn.disabled = true;\n status.textContent = '> processing...';\n\n fetch('/transform', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/x-www-form-urlencoded'}},\n body: 'text=' + encodeURIComponent(text)\n }})\n .then(r => r.json())\n .then(data => {{\n btn.disabled = false;\n if (data.tasks && data.tasks.length > 0) {{\n transformedTasks = data.tasks;\n status.textContent = '> ' + data.tasks.length + ' tasks generated';\n renderPreview(data.tasks);\n }} else {{\n status.textContent = '> ' + (data.error || 'No tasks generated');\n }}\n }})\n .catch(err => {{\n btn.disabled = false;\n status.textContent = '> error: ' + err.message;\n }});\n }}\n\n function renderPreview(tasks) {{\n const container = document.getElementById('preview-tasks');\n container.innerHTML = '';\n tasks.forEach((t, i) => {{\n const card = document.createElement('div');\n card.className = 'task';\n card.style.cssText = 'min-height:auto';\n card.innerHTML = '<div class=\"task-top-row\"><input type=\"checkbox\" checked data-idx=\"' + i + '\" style=\"width:16px;height:16px;cursor:pointer\"></div><div class=\"task-content\"><div class=\"task-title\">' + escHtml(t.title) + '</div><div class=\"task-desc-preview\">' + escHtml(t.description || '') + '</div></div>';\n container.appendChild(card);\n }});\n document.getElementById('transform-preview').style.display = 'block';\n }}\n\n function confirmTransform() {{\n const checks = document.querySelectorAll('#preview-tasks input[type=checkbox]');\n const selected = [];\n checks.forEach(cb => {{\n if (cb.checked) selected.push(transformedTasks[parseInt(cb.dataset.idx)]);\n }});\n if (selected.length === 0) return;\n fetch('/transform-confirm', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/json'}},\n body: JSON.stringify({{tasks: selected}})\n }})\n .then(r => r.json())\n .then(data => {{\n if (data.ok) {{\n document.getElementById('transform-status').textContent = '> ' + data.added + ' tasks added!';\n document.getElementById('transform-preview').style.display = 'none';\n document.getElementById('transform-input').value = '';\n setTimeout(() => location.reload(), 1000);\n }}\n }});\n }}\n </script>\n '''\n\n def build_task_detail_page(self, task_id):\n \"\"\"Full task detail page with logs, metrics, sessions\"\"\"\n tasks_mgr = TaskManager(PROJECT_DIR)\n all_tasks = tasks_mgr.get_tasks()\n task = None\n for t in all_tasks:\n if t.id == task_id:\n task = t\n break\n if not task:\n return '<div class=\"main\"><h1>Task not found</h1><a href=\"/tasks\" class=\"back-link\"><i class=\"bi bi-arrow-left\"></i> Back to Tasks</a></div>'\n\n # Status badge\n badge_cls = task.status.replace(' ', '_')\n status_label = task.status.replace('_', ' ').title()\n\n # Task info\n desc = task.description or 'No description'\n criteria = task.success_criteria or 'N/A'\n phase = getattr(task, 'phase', '') or 'N/A'\n blocked_reason = getattr(task, 'blocked_reason', '') or ''\n created = task.created_at[:16].replace('T', ' ') if task.created_at else 'N/A'\n completed = task.completed_at[:16].replace('T', ' ') if task.completed_at else '—'\n pri = getattr(task, 'priority', 0) or 0\n\n # Check if this is the active task\n is_active = False\n active_task_id = _get_current_task_id()\n if AGENT_RUNNING and active_task_id == task_id:\n is_active = True\n\n # Filtered logs: if active task, show ALL logs (including untagged)\n # If historical, filter by task_id match only\n if is_active:\n task_logs = [e for e in AGENT_LOG_BUFFER if e.get('task_id') in (task_id, None)]\n else:\n task_logs = [e for e in AGENT_LOG_BUFFER if e.get('task_id') == task_id]\n\n # Log type colors and labels\n label_map = {\n 'read': 'READ', 'edit': 'EDIT', 'write': 'WRITE',\n 'bash': 'BASH', 'thinking': 'THINK', 'text': 'OUT',\n 'metric': 'METRIC', 'verify': 'CHECK',\n }\n color_map = {\n 'read': '#89b4fa', 'edit': '#DA7756', 'write': '#2E8B57',\n 'bash': '#b392f0', 'thinking': '#D4940A', 'text': '#6B6B6B',\n 'metric': '#89dceb', 'verify': '#2E8B57',\n }\n\n # Derive metrics from logs\n tool_count = len([e for e in task_logs if e.get('type') in ('read', 'edit', 'write', 'bash')])\n time_span = '—'\n if task_logs:\n time_span = f\"{task_logs[0]['time']} — {task_logs[-1]['time']}\"\n\n # Live metrics from running agent\n live_tokens_in = 0\n live_tokens_out = 0\n if is_active and AGENT_LOOP and hasattr(AGENT_LOOP, 'get_session_metrics'):\n lm = AGENT_LOOP.get_session_metrics()\n live_tokens_in = lm.get('tokens_in', 0)\n live_tokens_out = lm.get('tokens_out', 0)\n\n # Sessions from checkpoint archives\n sessions_html = ''\n session_count = 0\n total_tokens_in = live_tokens_in\n total_tokens_out = live_tokens_out\n checkpoints_dir = PROJECT_DIR / '.a1' / 'checkpoints'\n if checkpoints_dir.exists():\n for f in sorted(checkpoints_dir.glob('session_*.json')):\n try:\n data = json.loads(f.read_text())\n if data.get('current_task') == task_id:\n session_count += 1\n sn = data.get('session', '?')\n sm = data.get('session_metrics', {})\n tok_in = sm.get('tokens_in', 0)\n tok_out = sm.get('tokens_out', 0)\n dur = sm.get('session_duration', 0)\n tools = sm.get('tools_used', 0)\n total_tokens_in += tok_in\n total_tokens_out += tok_out\n dur_str = f'{dur // 60}m {dur % 60}s' if dur >= 60 else f'{dur}s'\n def _ft(n):\n if n >= 1000000: return f'{n/1000000:.1f}M'\n if n >= 1000: return f'{n/1000:.1f}K'\n return str(n)\n sessions_html += f'''\n <div class=\"td-session-item\">\n <span class=\"td-session-num\">Session #{sn}</span>\n <div class=\"td-session-meta\">\n <span><i class=\"bi bi-lightning-charge\"></i> {_ft(tok_in)}/{_ft(tok_out)}</span>\n <span><i class=\"bi bi-stopwatch\"></i> {dur_str}</span>\n <span><i class=\"bi bi-tools\"></i> {tools} tools</span>\n </div>\n <span class=\"status {'status-completed' if data.get('status') == 'COMPLETED' else 'status-running'}\"\n style=\"font-size:11px;padding:3px 10px\">{esc(data.get('status', '?'))}</span>\n </div>'''\n except Exception:\n pass\n\n # Format token totals\n def _ft(n):\n if n >= 1000000: return f'{n/1000000:.1f}M'\n if n >= 1000: return f'{n/1000:.1f}K'\n return str(n)\n\n # Build log entries HTML\n log_html = ''\n for e in task_logs[-100:]:\n etype = e.get('type', 'text')\n label = label_map.get(etype, 'LOG')\n color = color_map.get(etype, '#6B6B6B')\n log_html += f'''<div class=\"td-log-entry\">\n <span class=\"td-log-time\">{esc(e[\"time\"])}</span>\n <span class=\"td-log-icon\" style=\"background:{color}\"></span>\n <span class=\"td-log-label\" style=\"color:{color}\">{label}</span>\n <span class=\"td-log-text\">{esc(e[\"line\"][:200])}</span>\n </div>'''\n\n active_dot = '<span style=\"display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--success);margin-left:8px;animation:pulse 1.5s infinite\"></span>' if is_active else ''\n\n return f'''\n <a href=\"/tasks\" class=\"back-link\"><i class=\"bi bi-arrow-left\"></i> Back to Tasks</a>\n\n <div class=\"task-detail-header\">\n <h1>{esc(task.title)}{active_dot}</h1>\n <span class=\"td-badge {badge_cls}\">{status_label}</span>\n {f'<span style=\"color:var(--text-secondary);font-size:13px\">Priority #{pri}</span>' if pri else ''}\n {'<button class=\"btn-primary\" id=\"start-task-btn\" onclick=\"startTask()\" style=\"margin-left:auto\"><i class=\"bi bi-play-fill\"></i> Start Task</button>' if task.status in ('pending', 'in_progress') and not is_active else ''}\n {'<button class=\"btn-secondary\" id=\"stop-task-btn\" onclick=\"stopTask()\" style=\"margin-left:8px\"><i class=\"bi bi-stop-fill\"></i> Stop</button>' if is_active else ''}\n {'<button class=\"btn-danger\" onclick=\"deleteTask()\" style=\"margin-left:8px\"><i class=\"bi bi-trash\"></i> Delete</button>' if not is_active else ''}\n </div>\n\n <div class=\"td-grid\">\n <div class=\"td-info\">\n <div class=\"td-info-label\">Description</div>\n <div class=\"td-info-value\">{esc(desc)}</div>\n <div class=\"td-info-label\">Success Criteria</div>\n <div class=\"td-info-value\">{esc(criteria)}</div>\n {f'<div class=\"td-info-label\" style=\"color:var(--danger)\">Blocked Reason</div><div class=\"td-info-value\" style=\"color:var(--danger);font-weight:600\">{esc(blocked_reason)}</div>' if blocked_reason else ''}\n <div class=\"td-info-label\">Phase</div>\n <div class=\"td-info-value\">{esc(phase)}</div>\n <div class=\"td-info-label\">Created</div>\n <div class=\"td-info-value\">{esc(created)}</div>\n <div class=\"td-info-label\">Completed</div>\n <div class=\"td-info-value\">{esc(completed)}</div>\n </div>\n <div class=\"td-metrics\">\n <div class=\"td-metric\">\n <div class=\"td-metric-val\">{tool_count}</div>\n <div class=\"td-metric-label\">Tool Calls</div>\n </div>\n <div class=\"td-metric\">\n <div class=\"td-metric-val\">{session_count}</div>\n <div class=\"td-metric-label\">Sessions</div>\n </div>\n <div class=\"td-metric\">\n <div class=\"td-metric-val\">{_ft(total_tokens_in)}</div>\n <div class=\"td-metric-label\">Tokens In</div>\n </div>\n <div class=\"td-metric\">\n <div class=\"td-metric-val\">{_ft(total_tokens_out)}</div>\n <div class=\"td-metric-label\">Tokens Out</div>\n </div>\n </div>\n </div>\n\n <div class=\"td-log-section\">\n <div class=\"td-log-header\">\n <span><i class=\"bi bi-terminal\"></i> Execution Log</span>\n <span style=\"font-size:12px;color:var(--text-secondary)\">{len(task_logs)} entries{' — LIVE' if is_active else ''}</span>\n </div>\n <div class=\"td-log-body\" id=\"task-log-body\">\n {log_html if log_html else '<div class=\"td-log-empty\"><i class=\"bi bi-inbox\"></i><br>No log entries for this task yet</div>'}\n </div>\n </div>\n\n {f\"\"\"<div class=\"td-sessions\">\n <div class=\"td-sessions-header\"><i class=\"bi bi-clock-history\"></i> Session History</div>\n {sessions_html}\n </div>\"\"\" if sessions_html else ''}\n\n <script>\n var taskPageId = '{task_id}';\n var taskLogIndex = 0;\n\n function startTask() {{\n var btn = document.getElementById('start-task-btn');\n if (btn) btn.disabled = true;\n fetch('/start-task/' + taskPageId, {{method: 'POST'}})\n .then(r => r.json())\n .then(d => {{\n if (btn) btn.textContent = 'Running...';\n }}).catch(() => {{ if (btn) btn.disabled = false; }});\n }}\n\n function stopTask() {{\n fetch('/stop', {{method: 'POST'}}).then(() => location.reload());\n }}\n\n function deleteTask() {{\n if (!confirm('Delete this task?')) return;\n fetch('/delete-task/' + taskPageId, {{method: 'POST'}})\n .then(() => {{ window.location.href = '/tasks'; }});\n }}\n\n function updateTaskLog() {{\n fetch('/api/log?since=' + taskLogIndex)\n .then(r => r.json())\n .then(data => {{\n if (data.entries && data.entries.length > 0) {{\n // Show logs for this task_id OR untagged (null) if this is the active task\n var filtered = data.entries.filter(e => e.task_id === taskPageId || e.task_id === null || e.task_id === undefined);\n if (filtered.length > 0) {{\n var container = document.getElementById('task-log-body');\n var emptyEl = container.querySelector('.td-log-empty');\n if (emptyEl) emptyEl.remove();\n var labelMap = {{'read':'READ','edit':'EDIT','write':'WRITE','bash':'BASH','thinking':'THINK','text':'OUT','metric':'METRIC','verify':'CHECK'}};\n var colorMap = {{'read':'#89b4fa','edit':'#DA7756','write':'#2E8B57','bash':'#b392f0','thinking':'#D4940A','text':'#6B6B6B','metric':'#89dceb','verify':'#2E8B57'}};\n filtered.forEach(e => {{\n var et = e.type || 'text';\n var label = labelMap[et] || 'LOG';\n var color = colorMap[et] || '#6B6B6B';\n var line = e.line || '';\n if (line.length > 200) line = line.substring(0, 200) + '...';\n var html = '<div class=\"td-log-entry\"><span class=\"td-log-time\">' + (e.time||'') + '</span><span class=\"td-log-icon\" style=\"background:' + color + '\"></span><span class=\"td-log-label\" style=\"color:' + color + '\">' + label + '</span><span class=\"td-log-text\">' + line.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</span></div>';\n container.insertAdjacentHTML('beforeend', html);\n }});\n container.scrollTop = container.scrollHeight;\n }}\n taskLogIndex = data.total;\n }}\n }}).catch(() => {{}});\n }}\n setInterval(updateTaskLog, 2000);\n // Auto-scroll on load\n var logBody = document.getElementById('task-log-body');\n if (logBody) logBody.scrollTop = logBody.scrollHeight;\n </script>\n '''\n\n def build_sessions_page(self):\n checkpoint = CheckpointManager(PROJECT_DIR)\n cp = checkpoint.load()\n\n sessions_html = ''\n checkpoints_dir = PROJECT_DIR / '.a1' / 'checkpoints'\n if checkpoints_dir.exists():\n for f in sorted(checkpoints_dir.glob('session_*.json'), reverse=True)[:10]:\n try:\n data = json.loads(f.read_text())\n session_num = data.get('session', '?')\n status = data.get('status', 'Unknown')\n files = len(data.get('files_modified', []))\n\n # Status badge color\n if status == 'COMPLETED':\n badge_cls = 'status-completed'\n elif status == 'WORKING':\n badge_cls = 'status-running'\n else:\n badge_cls = 'status-stopped'\n\n # Extract metrics from checkpoint data\n sm = data.get('session_metrics', {})\n tok_in = sm.get('tokens_in', 0)\n tok_out = sm.get('tokens_out', 0)\n duration = sm.get('session_duration', 0)\n tools = sm.get('tools_used', 0)\n\n # Format duration\n dur_str = f'{duration // 60}m {duration % 60}s' if duration >= 60 else f'{duration}s'\n\n # Format tokens\n def _fmt_tok(n):\n if n >= 1000000: return f'{n/1000000:.1f}M'\n if n >= 1000: return f'{n/1000:.1f}K'\n return str(n)\n\n sessions_html += f'''\n <div class=\"session-card\">\n <div class=\"session-header\">\n <span class=\"session-title\">Session #{session_num}</span>\n <span class=\"status {badge_cls}\">{esc(status)}</span>\n </div>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-file-earmark-code\"></i> Files</span>\n <span class=\"meta-value\">{files}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-check2-square\"></i> Task</span>\n <span class=\"meta-value\">{esc(data.get('current_task', 'N/A'))}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-lightning-charge\"></i> Tokens</span>\n <span class=\"meta-value\">{_fmt_tok(tok_in)} / {_fmt_tok(tok_out)}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-stopwatch\"></i> Duration</span>\n <span class=\"meta-value\">{dur_str}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-tools\"></i> Tool Calls</span>\n <span class=\"meta-value\">{tools}</span>\n </div>\n </div>\n </div>\n '''\n except Exception:\n pass\n\n # Current session status badge\n if AGENT_RUNNING:\n cur_badge = 'status-running'\n cur_label = '<i class=\"bi bi-play-circle-fill\"></i> Running'\n elif cp.get('status') == 'COMPLETED':\n cur_badge = 'status-completed'\n cur_label = '<i class=\"bi bi-check-circle-fill\"></i> Completed'\n elif cp.get('status') == 'WORKING':\n cur_badge = 'status-running'\n cur_label = '<i class=\"bi bi-arrow-repeat\"></i> Working'\n else:\n cur_badge = 'status-stopped'\n cur_label = '<i class=\"bi bi-stop-circle-fill\"></i> Idle'\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Sessions</h1>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n\n <div class=\"session-card\" style=\"border-left: 3px solid var(--accent)\">\n <div class=\"session-header\">\n <span class=\"session-title\">Current Session #{cp.get('session', 0)}</span>\n <span class=\"status {cur_badge}\">{cur_label}</span>\n </div>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-flag\"></i> Status</span>\n <span class=\"meta-value\">{esc(cp.get('status', 'Not started'))}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-pie-chart\"></i> Context</span>\n <span class=\"meta-value\">{cp.get('context_percent', 0)}%</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-check2-square\"></i> Current Task</span>\n <span class=\"meta-value\">{esc(cp.get('current_task', 'None'))}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\"><i class=\"bi bi-file-earmark-code\"></i> Files Modified</span>\n <span class=\"meta-value\">{len(cp.get('files_modified', []))}</span>\n </div>\n </div>\n </div>\n\n <h3 style=\"margin: 24px 0 16px; color: var(--text-secondary); font-size: 14px; text-transform: uppercase; letter-spacing: 1px\">Previous Sessions</h3>\n {sessions_html if sessions_html else '<div class=\"empty\"><i class=\"bi bi-clock-history\"></i><p>No previous sessions</p></div>'}\n '''\n\n def build_log_page(self):\n # Icons per status type\n status_icons = {\n 'success': '<i class=\"bi bi-check-circle-fill\" style=\"color:var(--success)\"></i>',\n 'error': '<i class=\"bi bi-x-circle-fill\" style=\"color:var(--danger)\"></i>',\n 'warning': '<i class=\"bi bi-exclamation-triangle-fill\" style=\"color:var(--warning)\"></i>',\n 'info': '<i class=\"bi bi-info-circle-fill\" style=\"color:var(--accent)\"></i>',\n }\n\n activity_html = ''\n for a in reversed(ACTIVITY_LOG):\n icon = status_icons.get(a['status'], status_icons['info'])\n activity_html += f'''\n <div class=\"activity-item\">\n <span class=\"activity-time\">{esc(a['time'])}</span>\n <span style=\"flex-shrink:0\">{icon}</span>\n <span class=\"activity-text\">\n <strong>{esc(a['action'])}</strong>\n <span class=\"activity-details\">{esc(a['details'])}</span>\n </span>\n </div>\n '''\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Activity Log</h1>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n\n <div class=\"activity\">\n <div class=\"activity-header\">\n All Activity\n <span style=\"font-weight:400;color:var(--text-secondary);font-size:13px;margin-left:8px\">{len(ACTIVITY_LOG)} entries</span>\n </div>\n <div class=\"activity-list\" style=\"max-height:none\">\n {activity_html if activity_html else '<div class=\"empty\"><i class=\"bi bi-clock-history\"></i><p>No activity yet</p></div>'}\n </div>\n </div>\n '''\n\n def build_commits_page(self):\n # Get git log with dates and full formatting\n import subprocess\n commits_html = ''\n try:\n result = subprocess.run(\n ['git', 'log', '--format=%h|%s|%cr|%an', '-20'],\n cwd=PROJECT_DIR,\n capture_output=True,\n text=True\n )\n if result.returncode == 0:\n for line in result.stdout.strip().split('\\n'):\n if line and '|' in line:\n parts = line.split('|', 3)\n hash_short = parts[0] if len(parts) > 0 else ''\n msg = parts[1] if len(parts) > 1 else ''\n rel_time = parts[2] if len(parts) > 2 else ''\n author = parts[3] if len(parts) > 3 else ''\n\n # Split commit message: first line = title, rest = body\n msg_title = esc(msg)\n\n # Icon based on conventional commit prefix\n if msg.startswith('feat'):\n icon_cls = 'bi-plus-circle'\n icon_color = 'var(--success)'\n elif msg.startswith('fix'):\n icon_cls = 'bi-bug'\n icon_color = 'var(--danger)'\n elif msg.startswith('docs') or msg.startswith('doc'):\n icon_cls = 'bi-file-text'\n icon_color = 'var(--accent)'\n elif msg.startswith('refactor') or msg.startswith('chore'):\n icon_cls = 'bi-arrow-repeat'\n icon_color = 'var(--warning)'\n elif msg.startswith('test'):\n icon_cls = 'bi-check2-circle'\n icon_color = '#89dceb'\n else:\n icon_cls = 'bi-git'\n icon_color = 'var(--text-secondary)'\n\n commits_html += f'''\n <div class=\"task\">\n <div class=\"task-check\" style=\"border-color:{icon_color};color:{icon_color}\"><i class=\"bi {icon_cls}\"></i></div>\n <div class=\"task-content\">\n <div class=\"commit-msg\">{msg_title}</div>\n <div style=\"display:flex;gap:12px;align-items:center;margin-top:4px\">\n <span class=\"commit-hash\">{esc(hash_short)}</span>\n <span class=\"commit-time\">{esc(rel_time)}</span>\n <span class=\"commit-time\">{esc(author)}</span>\n </div>\n </div>\n </div>\n '''\n except Exception:\n pass\n\n # Branch info\n branch = ''\n try:\n result = subprocess.run(\n ['git', 'branch', '--show-current'],\n cwd=PROJECT_DIR, capture_output=True, text=True\n )\n if result.returncode == 0:\n branch = result.stdout.strip()\n except Exception:\n pass\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Commits</h1>\n <div class=\"header-actions\">\n {f'<span style=\"font-size:13px;color:var(--text-secondary)\"><i class=\"bi bi-diagram-2\"></i> {esc(branch)}</span>' if branch else ''}\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n </div>\n\n <div class=\"task-list\">\n <div class=\"task-header\">\n <h3>Recent Commits</h3>\n </div>\n {commits_html if commits_html else '<div class=\"empty\"><i class=\"bi bi-git\"></i><p>No commits found</p></div>'}\n </div>\n '''\n\n def build_settings_page(self):\n from .config import Config\n config = Config(PROJECT_DIR)\n data = config.get_all()\n provider = esc(data.get(\"provider\", \"claude-max\"))\n api_key_masked = esc(config.mask_api_key(data.get(\"api_key\")) or \"\")\n ollama_host = esc(data.get(\"ollama_host\", \"http://localhost:11434\"))\n ollama_model = esc(data.get(\"ollama_model\", \"qwen3:30b-a3b\"))\n max_sessions = data.get(\"max_sessions\", 100)\n max_turns = data.get(\"max_turns\", 25)\n session_delay = data.get(\"session_delay\", 5)\n context_threshold = data.get(\"context_threshold\", 0.70)\n\n # Provider options with selected state\n providers = [\n (\"claude-max\", \"Claude Subscription (Pro/Max — via Claude Code CLI)\"),\n (\"claude-api\", \"Claude API (Anthropic API key)\"),\n (\"ollama\", \"Ollama (local models)\"),\n ]\n options_html = \"\"\n for val, label in providers:\n sel = ' selected' if val == provider else ''\n options_html += f'<option value=\"{val}\"{sel}>{label}</option>'\n\n return f'''\n <div class=\"header\">\n <h1 class=\"page-title\">// Settings</h1>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n\n <div class=\"card\" style=\"margin-bottom:16px\">\n <div class=\"card-title\"><i class=\"bi bi-cpu\"></i> Provider</div>\n <select id=\"cfg-provider\" onchange=\"onProviderChange(this.value)\" style=\"margin-top:8px\">\n {options_html}\n </select>\n <div id=\"provider-badge\" class=\"settings-hint\">\n {self._provider_badge(provider)}\n </div>\n </div>\n\n <div class=\"card\" id=\"card-apikey\" style=\"margin-bottom:16px;{'display:none' if provider != 'claude-api' else ''}\">\n <div class=\"card-title\"><i class=\"bi bi-key\"></i> API Key</div>\n <div style=\"display:flex;gap:8px;margin-top:8px\">\n <input type=\"password\" id=\"cfg-apikey\" placeholder=\"sk-ant-api03-...\"\n value=\"{api_key_masked}\" style=\"flex:1\">\n <button onclick=\"saveApiKey()\" class=\"btn-primary\" style=\"white-space:nowrap\">\n <i class=\"bi bi-check-lg\"></i> Save\n </button>\n </div>\n <div class=\"settings-hint\">\n Or set <code style=\"background:var(--bg-tertiary);padding:2px 6px;border-radius:4px\">ANTHROPIC_API_KEY</code> environment variable\n </div>\n </div>\n\n <div class=\"card\" id=\"card-ollama\" style=\"margin-bottom:16px;{'display:none' if provider != 'ollama' else ''}\">\n <div class=\"card-title\"><i class=\"bi bi-hdd-network\"></i> Ollama</div>\n <div style=\"margin-top:8px\">\n <label class=\"settings-label\">Host URL</label>\n <input type=\"text\" id=\"cfg-ollama-host\" value=\"{ollama_host}\" style=\"font-family:monospace;width:100%\">\n </div>\n <div style=\"margin-top:12px\">\n <label class=\"settings-label\">Model</label>\n <input type=\"text\" id=\"cfg-ollama-model\" value=\"{ollama_model}\" style=\"font-family:monospace;width:100%\">\n </div>\n <button onclick=\"saveOllamaConfig()\" class=\"btn-primary\" style=\"margin-top:12px\">\n <i class=\"bi bi-check-lg\"></i> Save\n </button>\n </div>\n\n <div class=\"card\" style=\"margin-bottom:16px\">\n <div class=\"card-title\"><i class=\"bi bi-sliders\"></i> Session</div>\n <div class=\"settings-grid\">\n <div>\n <label class=\"settings-label\">Max Sessions</label>\n <input type=\"number\" id=\"cfg-max-sessions\" value=\"{max_sessions}\" min=\"1\" max=\"1000\">\n </div>\n <div>\n <label class=\"settings-label\">Max Turns</label>\n <input type=\"number\" id=\"cfg-max-turns\" value=\"{max_turns}\" min=\"1\" max=\"100\">\n </div>\n <div>\n <label class=\"settings-label\">Session Delay (sec)</label>\n <input type=\"number\" id=\"cfg-delay\" value=\"{session_delay}\" min=\"0\" max=\"60\">\n </div>\n <div>\n <label class=\"settings-label\">Context Threshold</label>\n <input type=\"number\" id=\"cfg-threshold\" value=\"{context_threshold}\" min=\"0.1\" max=\"0.95\" step=\"0.05\">\n </div>\n </div>\n <button onclick=\"saveSessionConfig()\" class=\"btn-primary\" style=\"margin-top:12px\">\n <i class=\"bi bi-check-lg\"></i> Save\n </button>\n </div>\n\n <div class=\"card\" style=\"margin-bottom:16px\">\n <div class=\"card-title\"><i class=\"bi bi-moon-stars\"></i> Theme</div>\n <button onclick=\"toggleTheme()\" class=\"btn-secondary\" style=\"margin-top:8px\">\n <i class=\"bi bi-moon-stars\"></i> Toggle Dark/Light\n </button>\n </div>\n\n <div class=\"card\" style=\"margin-bottom:16px\">\n <div class=\"card-title\"><i class=\"bi bi-file-earmark-code\"></i> Config File</div>\n <p style=\"margin-top:8px;font-family:monospace\" class=\"commit-hash\">\n {esc(str(config.path))}\n </p>\n </div>\n\n <script>\n function showToast(msg) {{\n const t = document.createElement('div');\n t.className = 'toast';\n t.textContent = msg;\n document.body.appendChild(t);\n setTimeout(() => t.remove(), 2500);\n }}\n\n function onProviderChange(val) {{\n document.getElementById('card-apikey').style.display = val === 'claude-api' ? 'block' : 'none';\n document.getElementById('card-ollama').style.display = val === 'ollama' ? 'block' : 'none';\n fetch('/api/config', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/json'}},\n body: JSON.stringify({{provider: val}})\n }}).then(r => r.json()).then(d => {{\n if (d.ok) {{\n let badge = document.getElementById('provider-badge');\n if (val === 'claude-max') badge.innerHTML = '<span style=\"color:var(--success)\">Active</span>';\n else badge.innerHTML = '<span style=\"color:var(--warning)\">EXPERIMENTAL</span>';\n showToast('Provider saved: ' + val);\n }}\n }});\n }}\n\n function saveApiKey() {{\n let key = document.getElementById('cfg-apikey').value;\n if (!key || key.includes('...')) {{ showToast('Enter the full API key'); return; }}\n fetch('/api/config', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/json'}},\n body: JSON.stringify({{api_key: key}})\n }}).then(r => r.json()).then(d => {{\n if (d.ok) showToast('API key saved');\n }});\n }}\n\n function saveOllamaConfig() {{\n let host = document.getElementById('cfg-ollama-host').value;\n let model = document.getElementById('cfg-ollama-model').value;\n fetch('/api/config', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/json'}},\n body: JSON.stringify({{ollama_host: host, ollama_model: model}})\n }}).then(r => r.json()).then(d => {{\n if (d.ok) showToast('Ollama config saved');\n }});\n }}\n\n function saveSessionConfig() {{\n let cfg = {{\n max_sessions: parseInt(document.getElementById('cfg-max-sessions').value),\n max_turns: parseInt(document.getElementById('cfg-max-turns').value),\n session_delay: parseInt(document.getElementById('cfg-delay').value),\n context_threshold: parseFloat(document.getElementById('cfg-threshold').value),\n }};\n fetch('/api/config', {{\n method: 'POST',\n headers: {{'Content-Type': 'application/json'}},\n body: JSON.stringify(cfg)\n }}).then(r => r.json()).then(d => {{\n if (d.ok) showToast('Session config saved');\n }});\n }}\n </script>\n '''\n\n def _provider_badge(self, provider: str) -> str:\n \"\"\"Generate provider status badge HTML\"\"\"\n if provider == \"claude-max\":\n return '<span style=\"color:#a6e3a1\">Active</span>'\n return '<span style=\"color:#f9e2af\">EXPERIMENTAL</span> — untested'\n\n def build_transform_page(self):\n return '''\n <div class=\"header\">\n <h1 class=\"page-title\">// Transform</h1>\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\">\n <i class=\"bi bi-moon-stars\"></i>\n </button>\n </div>\n\n <div class=\"cards\" style=\"grid-template-columns:repeat(3,1fr);margin-bottom:24px\">\n <div class=\"card\" style=\"text-align:center;padding:16px\">\n <div style=\"font-size:24px;margin-bottom:8px\"><i class=\"bi bi-pencil-square\" style=\"color:var(--accent)\"></i></div>\n <div style=\"font-size:13px;font-weight:600\">1. Write</div>\n <div style=\"font-size:12px;color:var(--text-secondary)\">Enter raw text or ideas</div>\n </div>\n <div class=\"card\" style=\"text-align:center;padding:16px\">\n <div style=\"font-size:24px;margin-bottom:8px\"><i class=\"bi bi-magic\" style=\"color:var(--warning)\"></i></div>\n <div style=\"font-size:13px;font-weight:600\">2. Transform</div>\n <div style=\"font-size:12px;color:var(--text-secondary)\">AI breaks into tasks</div>\n </div>\n <div class=\"card\" style=\"text-align:center;padding:16px\">\n <div style=\"font-size:24px;margin-bottom:8px\"><i class=\"bi bi-check2-all\" style=\"color:var(--success)\"></i></div>\n <div style=\"font-size:13px;font-weight:600\">3. Confirm</div>\n <div style=\"font-size:12px;color:var(--text-secondary)\">Review and add to queue</div>\n </div>\n </div>\n\n <div class=\"form-section\">\n <h3>Raw Text to Tasks</h3>\n <p style=\"color: var(--text-secondary); margin-bottom:16px; font-size:13px\">\n Enter raw text, notes, or ideas — AI will break them into structured tasks.\n </p>\n <textarea id=\"transform-input\" rows=\"6\" placeholder=\"Example:&#10;Add login page with email/password fields&#10;Registration form with validation&#10;Password reset flow via email&#10;Write unit tests for auth module\"></textarea>\n <div style=\"margin-top:12px;display:flex;align-items:center;gap:12px\">\n <button class=\"btn-primary\" onclick=\"doTransform()\" id=\"transform-btn\">\n <i class=\"bi bi-magic\"></i> AI Transform\n </button>\n <span id=\"transform-status\" style=\"font-size:13px;color:var(--text-secondary)\"></span>\n </div>\n </div>\n\n <div id=\"transform-preview\" style=\"display:none;margin-top:24px\">\n <div class=\"task-list\">\n <div class=\"task-header\">\n <h3>Preview Tasks</h3>\n <button class=\"btn-success\" onclick=\"confirmTransform()\">\n <i class=\"bi bi-check-all\"></i> Add Selected\n </button>\n </div>\n <div id=\"preview-tasks\"></div>\n </div>\n </div>\n\n <script>\n let transformedTasks = [];\n\n function doTransform() {\n const text = document.getElementById('transform-input').value.trim();\n if (!text) return;\n const btn = document.getElementById('transform-btn');\n const status = document.getElementById('transform-status');\n btn.disabled = true;\n status.textContent = 'Thinking...';\n\n fetch('/transform', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'text=' + encodeURIComponent(text)\n })\n .then(r => r.json())\n .then(data => {\n btn.disabled = false;\n if (data.tasks && data.tasks.length > 0) {\n transformedTasks = data.tasks;\n status.textContent = data.tasks.length + ' tasks generated';\n renderPreview(data.tasks);\n } else {\n status.textContent = data.error || 'No tasks generated';\n }\n })\n .catch(err => {\n btn.disabled = false;\n status.textContent = 'Error: ' + err.message;\n });\n }\n\n function renderPreview(tasks) {\n const container = document.getElementById('preview-tasks');\n container.innerHTML = '';\n tasks.forEach((t, i) => {\n container.innerHTML += '<div class=\"task\"><div class=\"task-check\"><input type=\"checkbox\" checked data-idx=\"' + i + '\" style=\"width:18px;height:18px;cursor:pointer\"></div><div class=\"task-content\"><div class=\"task-title\">' + escHtml(t.title) + '</div><div class=\"task-meta\">' + escHtml(t.description || '') + '</div></div></div>';\n });\n document.getElementById('transform-preview').style.display = 'block';\n }\n\n function confirmTransform() {\n const checks = document.querySelectorAll('#preview-tasks input[type=checkbox]');\n const selected = [];\n checks.forEach(cb => {\n if (cb.checked) selected.push(transformedTasks[parseInt(cb.dataset.idx)]);\n });\n if (selected.length === 0) return;\n fetch('/transform-confirm', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({tasks: selected})\n })\n .then(r => r.json())\n .then(data => {\n if (data.ok) {\n document.getElementById('transform-status').textContent = data.added + ' tasks added!';\n document.getElementById('transform-preview').style.display = 'none';\n document.getElementById('transform-input').value = '';\n }\n });\n }\n </script>\n '''\n\n def _transform_text(self, raw_text: str) -> dict:\n \"\"\"Use Claude to break raw text into structured tasks\"\"\"\n import os\n import subprocess\n\n prompt = f'''Break the following text into structured tasks for a software project.\nReturn ONLY valid JSON array, no other text. Each task object must have:\n- \"title\": short task title (imperative, e.g. \"Add login page\")\n- \"description\": 1-2 sentence description of what needs to be done\n- \"success_criteria\": how to verify the task is done (e.g. \"pytest passes\", \"page renders at /login\", \"API returns 200\")\n\nText to transform:\n{raw_text}\n\nReturn format: [{{\"title\": \"...\", \"description\": \"...\", \"success_criteria\": \"...\"}}, ...]'''\n\n try:\n env = os.environ.copy()\n env.pop(\"CLAUDECODE\", None)\n result = subprocess.run(\n [\"claude\", \"-p\", prompt, \"--max-turns\", \"1\", \"--no-session-persistence\", \"--dangerously-skip-permissions\"],\n cwd=str(PROJECT_DIR),\n env=env,\n capture_output=True,\n text=True,\n timeout=300,\n )\n output = result.stdout.strip()\n # Try to extract JSON from output\n start = output.find('[')\n end = output.rfind(']')\n if start >= 0 and end > start:\n tasks = json.loads(output[start:end+1])\n return {\"tasks\": tasks}\n return {\"tasks\": [], \"error\": \"Could not parse AI response\"}\n except subprocess.TimeoutExpired:\n return {\"tasks\": [], \"error\": \"AI request timed out (5 min)\"}\n except FileNotFoundError:\n return {\"tasks\": [], \"error\": \"Claude CLI not found\"}\n except Exception as e:\n return {\"tasks\": [], \"error\": str(e)}\n\n def send_json_status(self):\n checkpoint = CheckpointManager(PROJECT_DIR)\n tasks = TaskManager(PROJECT_DIR)\n\n # Get live metrics from agent loop if running\n metrics = {}\n if AGENT_LOOP and hasattr(AGENT_LOOP, 'get_session_metrics'):\n metrics = AGENT_LOOP.get_session_metrics()\n else:\n # Try to load from checkpoint\n cp = checkpoint.load()\n metrics = cp.get('session_metrics', {})\n\n data = {\n 'checkpoint': checkpoint.load(),\n 'tasks': [t.to_dict() for t in tasks.get_tasks()],\n 'progress': tasks.get_progress(),\n 'running': AGENT_RUNNING,\n 'activity': ACTIVITY_LOG[-10:],\n 'metrics': metrics,\n }\n\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps(data).encode('utf-8'))\n\n def send_json_log(self):\n \"\"\"Return agent log entries since a given index\"\"\"\n query = urlparse(self.path).query\n params = parse_qs(query)\n since = int(params.get('since', ['0'])[0])\n\n entries = AGENT_LOG_BUFFER[since:]\n data = {\n 'entries': entries,\n 'total': len(AGENT_LOG_BUFFER),\n 'since': since,\n }\n\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps(data).encode('utf-8'))\n\n def send_json_task(self, task_id):\n \"\"\"Return task detail JSON with logs, metrics, sessions\"\"\"\n tasks_mgr = TaskManager(PROJECT_DIR)\n task = None\n for t in tasks_mgr.get_tasks():\n if t.id == task_id:\n task = t\n break\n if not task:\n self.send_response(404)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps({\"error\": \"Task not found\"}).encode('utf-8'))\n return\n\n # Filtered logs: if active task, include untagged logs\n is_active_task = AGENT_RUNNING and _get_current_task_id() == task_id\n if is_active_task:\n task_logs = [e for e in AGENT_LOG_BUFFER if e.get('task_id') in (task_id, None)]\n else:\n task_logs = [e for e in AGENT_LOG_BUFFER if e.get('task_id') == task_id]\n\n # Metrics from logs\n tool_types = {}\n for e in task_logs:\n et = e.get('type', 'text')\n tool_types[et] = tool_types.get(et, 0) + 1\n\n # Sessions from archives\n sessions = []\n checkpoints_dir = PROJECT_DIR / '.a1' / 'checkpoints'\n if checkpoints_dir.exists():\n for f in sorted(checkpoints_dir.glob('session_*.json')):\n try:\n data = json.loads(f.read_text())\n if data.get('current_task') == task_id:\n sessions.append({\n 'session': data.get('session'),\n 'status': data.get('status'),\n 'metrics': data.get('session_metrics', {}),\n })\n except Exception:\n pass\n\n result = {\n 'task': task.to_dict(),\n 'logs': task_logs[-200:],\n 'log_count': len(task_logs),\n 'metrics': {\n 'tool_types': tool_types,\n 'tool_count': sum(1 for e in task_logs if e.get('type') in ('read', 'edit', 'write', 'bash')),\n },\n 'sessions': sessions,\n 'is_active': AGENT_RUNNING and _get_current_task_id() == task_id,\n }\n\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps(result).encode('utf-8'))\n\n def send_json_config(self):\n \"\"\"Return current config (API key masked)\"\"\"\n from .config import Config, DEFAULTS\n config = Config(PROJECT_DIR)\n data = config.get_all()\n # Mask API key for security\n if data.get(\"api_key\"):\n data[\"api_key\"] = config.mask_api_key(data[\"api_key\"])\n data[\"_defaults\"] = DEFAULTS\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps(data).encode('utf-8'))\n\n def _render_log_entries(self):\n \"\"\"Render existing log buffer entries as HTML (terminal style)\"\"\"\n label_map = {\n 'read': 'READ', 'edit': 'EDIT', 'write': 'WRITE',\n 'bash': 'BASH', 'thinking': 'THINK', 'text': 'OUT',\n 'metric': 'METRIC', 'verify': 'CHECK',\n }\n color_map = {\n 'read': '#89b4fa', 'edit': '#fab387', 'write': '#a6e3a1',\n 'bash': '#cba6f7', 'thinking': '#f9e2af', 'text': '#6c7086',\n 'metric': '#89dceb', 'verify': '#a6e3a1',\n }\n html = ''\n for e in AGENT_LOG_BUFFER[-50:]:\n etype = e.get('type', 'text')\n label = label_map.get(etype, 'LOG')\n color = color_map.get(etype, '#6c7086')\n tid = e.get('task_id', '')\n tid_tag = f'<span style=\"color:var(--accent);font-size:10px;margin-right:4px\">[{esc(tid)}]</span>' if tid else ''\n html += f'<div class=\"log-entry\"><span class=\"px-icon\" style=\"background:{color}\"></span><span class=\"log-time\">{esc(e[\"time\"])}</span><span class=\"log-label\" style=\"color:{color}\">{label}</span>{tid_tag}<span class=\"log-text\">{esc(e[\"line\"][:150])}</span></div>'\n if not AGENT_LOG_BUFFER:\n html = '<div class=\"log-empty\">Waiting for agent output...<span class=\"log-cursor\"></span></div>'\n return html\n\n def _render_raw_log(self):\n \"\"\"Render raw log text for initial page load\"\"\"\n return esc('\\n'.join(e['line'] for e in AGENT_LOG_BUFFER[-50:]))\n\n def start_agent(self):\n global AGENT_RUNNING, AGENT_LOOP\n if not AGENT_RUNNING:\n AGENT_LOG_BUFFER.clear()\n\n # Pre-set current_task so logs get tagged from the start\n tasks_mgr = TaskManager(PROJECT_DIR)\n next_task = tasks_mgr.get_next_task()\n if next_task:\n checkpoint = CheckpointManager(PROJECT_DIR)\n cp = checkpoint.load()\n if not cp.get(\"current_task\"):\n cp[\"current_task\"] = next_task.id\n checkpoint.save(cp)\n _TASK_CACHE[\"task_id\"] = cp.get(\"current_task\") or (next_task.id if next_task else None)\n _TASK_CACHE[\"ts\"] = _time.time()\n\n log_activity(\"Agent started\", f\"task: {next_task.id if next_task else 'none'}\", \"success\")\n\n def run():\n global AGENT_RUNNING, AGENT_LOOP\n AGENT_RUNNING = True\n try:\n from .config import Config\n from .loop import SessionLoop\n config = Config(PROJECT_DIR)\n resolved = config.resolve()\n loop = SessionLoop(project_dir=PROJECT_DIR, **resolved)\n loop._log_callback = _on_agent_line\n AGENT_LOOP = loop\n loop.start()\n finally:\n AGENT_RUNNING = False\n AGENT_LOOP = None\n log_activity(\"Agent stopped\", \"\", \"warning\")\n\n thread = threading.Thread(target=run, daemon=True)\n thread.start()\n\n def start_task_agent(self, task_id):\n \"\"\"Start agent on a specific task — set current_task + in_progress, then start\"\"\"\n # Set task to in_progress\n tasks_mgr = TaskManager(PROJECT_DIR)\n tasks_mgr.start_task(task_id)\n\n # Set current_task in checkpoint so agent picks it up\n checkpoint = CheckpointManager(PROJECT_DIR)\n cp = checkpoint.load()\n cp[\"current_task\"] = task_id\n checkpoint.save(cp)\n\n # Update task cache immediately\n _TASK_CACHE[\"task_id\"] = task_id\n _TASK_CACHE[\"ts\"] = _time.time()\n\n log_activity(\"Task started\", task_id, \"success\")\n\n # Start agent for this one task only\n if not AGENT_RUNNING:\n self._start_single_task_agent(task_id)\n\n # Return JSON (called via fetch from task detail page)\n self.send_response(200)\n self.send_header('Content-Type', 'application/json')\n self.end_headers()\n self.wfile.write(json.dumps({\"ok\": True, \"task_id\": task_id}).encode('utf-8'))\n\n def _start_single_task_agent(self, task_id):\n \"\"\"Start agent for ONE task only (max_sessions=1)\"\"\"\n global AGENT_RUNNING, AGENT_LOOP\n if not AGENT_RUNNING:\n AGENT_LOG_BUFFER.clear()\n log_activity(\"Agent started\", f\"single task: {task_id}\", \"success\")\n\n def run():\n global AGENT_RUNNING, AGENT_LOOP\n AGENT_RUNNING = True\n try:\n from .config import Config\n from .loop import SessionLoop\n config = Config(PROJECT_DIR)\n resolved = config.resolve()\n resolved['max_sessions'] = 1\n loop = SessionLoop(project_dir=PROJECT_DIR, **resolved)\n loop._log_callback = _on_agent_line\n AGENT_LOOP = loop\n loop.start()\n finally:\n AGENT_RUNNING = False\n AGENT_LOOP = None\n log_activity(\"Agent stopped\", f\"task: {task_id}\", \"warning\")\n\n thread = threading.Thread(target=run, daemon=True)\n thread.start()\n\n def stop_agent(self):\n global AGENT_RUNNING, AGENT_LOOP\n if AGENT_LOOP:\n AGENT_LOOP.stop()\n AGENT_RUNNING = False\n log_activity(\"Agent stop requested\", \"\", \"warning\")\n\n\ndef find_free_port(start_port: int = 7331, max_attempts: int = 20) -> int:\n for offset in range(max_attempts):\n port = start_port + offset\n try:\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n s.bind(('0.0.0.0', port))\n return port\n except OSError:\n continue\n raise RuntimeError(f\"No free port in range {start_port}-{start_port + max_attempts}\")\n\n\ndef run_dashboard(project_dir: Path, port: int = None, open_browser: bool = True):\n global PROJECT_DIR\n PROJECT_DIR = Path(project_dir).resolve()\n\n a1_dir = PROJECT_DIR / '.a1'\n if not a1_dir.exists():\n a1_dir.mkdir(parents=True)\n (a1_dir / 'sessions').mkdir()\n (a1_dir / 'checkpoints').mkdir()\n\n if port is None:\n port = find_free_port(7331)\n else:\n try:\n port = find_free_port(port, max_attempts=1)\n except RuntimeError:\n port = find_free_port(7331)\n\n server = ThreadingHTTPServer(('0.0.0.0', port), DashboardHandler)\n\n url = f'http://localhost:{port}'\n print()\n print(' PocketCoder-A1 Dashboard')\n print(' -------------------------')\n print(f' URL: {url}')\n print(f' Project: {PROJECT_DIR}')\n print()\n print(' Press Ctrl+C to stop')\n print()\n\n log_activity(\"Dashboard started\", f\"Port {port}\", \"info\")\n\n if open_browser:\n webbrowser.open(url)\n\n try:\n server.serve_forever()\n except KeyboardInterrupt:\n print('\\\\nDashboard stopped')\n server.shutdown()\n\n\nif __name__ == '__main__':\n import sys\n project = sys.argv[1] if len(sys.argv) > 1 else '.'\n run_dashboard(project)\n"}}}}