{"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('''\n\n
\n \n \nNo tasks yet
No activity yet
No tasks yet
One task per line. Priorities auto-assigned.
\n \n\n Write raw text, notes, ideas — AI breaks them into structured tasks.\n
\n \nNo previous sessions
No activity yet
No commits found
\n {esc(str(config.path))}\n
\n\n Enter raw text, notes, or ideas — AI will break them into structured tasks.\n
\n \n