loki-mode 5.40.0 → 5.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +4 -4
- package/VERSION +1 -1
- package/autonomy/hooks/validate-bash.sh +12 -0
- package/autonomy/run.sh +63 -10
- package/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +44 -11
- package/dashboard/control.py +101 -2
- package/dashboard/registry.py +3 -2
- package/dashboard/server.py +67 -6
- package/dashboard/static/index.html +8 -8
- package/dashboard/telemetry.py +4 -1
- package/docs/INSTALLATION.md +1 -1
- package/events/emit.sh +22 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.40.
|
|
6
|
+
# Loki Mode v5.40.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -127,8 +127,8 @@ GROWTH ──[continuous improvement loop]──> GROWTH
|
|
|
127
127
|
- Load only 1-2 skill modules at a time (from skills/00-index.md)
|
|
128
128
|
- Use Task tool with subagents for exploration (isolates context)
|
|
129
129
|
- IF context feels heavy: Create `.loki/signals/CONTEXT_CLEAR_REQUESTED`
|
|
130
|
-
- **Context Window Tracking (v5.40.
|
|
131
|
-
- **Notification Triggers (v5.40.
|
|
130
|
+
- **Context Window Tracking (v5.40.1):** Dashboard gauge, timeline, and per-agent breakdown at `GET /api/context`
|
|
131
|
+
- **Notification Triggers (v5.40.1):** Configurable alerts when context exceeds thresholds, tasks fail, or budget limits hit. Manage via `GET/PUT /api/notifications/triggers`
|
|
132
132
|
|
|
133
133
|
---
|
|
134
134
|
|
|
@@ -262,4 +262,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
262
262
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
263
263
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
264
264
|
|
|
265
|
-
**v5.40.
|
|
265
|
+
**v5.40.1 | fix: 46-bug audit across all distributions, JSON injection, Docker mounts, auth hardening | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.40.
|
|
1
|
+
5.40.1
|
|
@@ -10,6 +10,9 @@ CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).ge
|
|
|
10
10
|
|
|
11
11
|
# Dangerous command patterns (matched anywhere in the command string)
|
|
12
12
|
# Safe paths like /tmp/ and relative paths (./) are excluded below
|
|
13
|
+
# NOTE: This is defense-in-depth, not a security boundary. Motivated attackers
|
|
14
|
+
# can bypass with advanced techniques (heredocs, printf, arbitrary string building).
|
|
15
|
+
# This hook catches common mistakes and simple bypass attempts.
|
|
13
16
|
BLOCKED_PATTERNS=(
|
|
14
17
|
"rm -rf /"
|
|
15
18
|
"rm -rf ~"
|
|
@@ -18,6 +21,15 @@ BLOCKED_PATTERNS=(
|
|
|
18
21
|
"mkfs[. ]"
|
|
19
22
|
"dd if=/dev/zero"
|
|
20
23
|
"chmod -R 777 /"
|
|
24
|
+
"eval.*base64"
|
|
25
|
+
"base64.*\|.*sh"
|
|
26
|
+
"base64.*\|.*bash"
|
|
27
|
+
"\$\(base64"
|
|
28
|
+
"eval.*\\\$\("
|
|
29
|
+
"curl.*\|.*sh"
|
|
30
|
+
"wget.*\|.*sh"
|
|
31
|
+
"curl.*\|.*bash"
|
|
32
|
+
"wget.*\|.*bash"
|
|
21
33
|
)
|
|
22
34
|
|
|
23
35
|
# Safe path patterns that override rm -rf / matches
|
package/autonomy/run.sh
CHANGED
|
@@ -2147,13 +2147,32 @@ init_loki_dir() {
|
|
|
2147
2147
|
|
|
2148
2148
|
# Clean up stale control files ONLY if no other session is running
|
|
2149
2149
|
# Deleting these while another session is active would destroy its signals
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2150
|
+
# Use flock if available to avoid TOCTOU race
|
|
2151
|
+
local lock_file=".loki/session.lock"
|
|
2152
|
+
local can_cleanup=false
|
|
2153
|
+
|
|
2154
|
+
if command -v flock >/dev/null 2>&1 && [ -f "$lock_file" ]; then
|
|
2155
|
+
# Try non-blocking lock - if we get it, no other session is running
|
|
2156
|
+
{
|
|
2157
|
+
if flock -n 201 2>/dev/null; then
|
|
2158
|
+
can_cleanup=true
|
|
2159
|
+
fi
|
|
2160
|
+
} 201>"$lock_file"
|
|
2161
|
+
else
|
|
2162
|
+
# Fallback: check PID file
|
|
2163
|
+
local existing_pid=""
|
|
2164
|
+
if [ -f ".loki/loki.pid" ]; then
|
|
2165
|
+
existing_pid=$(cat ".loki/loki.pid" 2>/dev/null)
|
|
2166
|
+
fi
|
|
2167
|
+
if [ -z "$existing_pid" ] || ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
2168
|
+
can_cleanup=true
|
|
2169
|
+
fi
|
|
2153
2170
|
fi
|
|
2154
|
-
|
|
2171
|
+
|
|
2172
|
+
if [ "$can_cleanup" = "true" ]; then
|
|
2155
2173
|
rm -f .loki/PAUSE .loki/STOP .loki/HUMAN_INPUT.md 2>/dev/null
|
|
2156
2174
|
rm -f .loki/loki.pid 2>/dev/null
|
|
2175
|
+
rm -f .loki/session.lock 2>/dev/null
|
|
2157
2176
|
fi
|
|
2158
2177
|
|
|
2159
2178
|
mkdir -p .loki/{state,queue,messages,logs,config,prompts,artifacts,scripts}
|
|
@@ -6945,16 +6964,50 @@ main() {
|
|
|
6945
6964
|
update_continuity
|
|
6946
6965
|
|
|
6947
6966
|
# Session lock: prevent concurrent sessions on same repo
|
|
6967
|
+
# Use flock for atomic locking to prevent TOCTOU race conditions
|
|
6948
6968
|
local pid_file=".loki/loki.pid"
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6969
|
+
local lock_file=".loki/session.lock"
|
|
6970
|
+
|
|
6971
|
+
# Try to acquire exclusive lock with flock (if available)
|
|
6972
|
+
if command -v flock >/dev/null 2>&1; then
|
|
6973
|
+
# Create lock file
|
|
6974
|
+
touch "$lock_file"
|
|
6975
|
+
|
|
6976
|
+
# Open FD 200 at process scope so flock persists for entire session lifetime
|
|
6977
|
+
# (block-scoped redirection would release the lock when the block exits)
|
|
6978
|
+
exec 200>"$lock_file"
|
|
6979
|
+
|
|
6980
|
+
# Try to acquire exclusive lock (non-blocking)
|
|
6981
|
+
if ! flock -n 200 2>/dev/null; then
|
|
6982
|
+
log_error "Another Loki session is already running (locked)"
|
|
6955
6983
|
log_error "Stop it first with: loki stop"
|
|
6956
6984
|
exit 1
|
|
6957
6985
|
fi
|
|
6986
|
+
|
|
6987
|
+
# Check PID file after acquiring lock
|
|
6988
|
+
if [ -f "$pid_file" ]; then
|
|
6989
|
+
local existing_pid
|
|
6990
|
+
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
6991
|
+
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
6992
|
+
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
6993
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
6994
|
+
log_error "Stop it first with: loki stop"
|
|
6995
|
+
exit 1
|
|
6996
|
+
fi
|
|
6997
|
+
fi
|
|
6998
|
+
else
|
|
6999
|
+
# Fallback to original behavior if flock not available
|
|
7000
|
+
log_warn "flock not available - using non-atomic PID check (race condition possible)"
|
|
7001
|
+
if [ -f "$pid_file" ]; then
|
|
7002
|
+
local existing_pid
|
|
7003
|
+
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
7004
|
+
# Skip if it's our own PID or parent PID (background mode writes PID before child starts)
|
|
7005
|
+
if [ -n "$existing_pid" ] && [ "$existing_pid" != "$$" ] && [ "$existing_pid" != "$PPID" ] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
7006
|
+
log_error "Another Loki session is already running (PID: $existing_pid)"
|
|
7007
|
+
log_error "Stop it first with: loki stop"
|
|
7008
|
+
exit 1
|
|
7009
|
+
fi
|
|
7010
|
+
fi
|
|
6958
7011
|
fi
|
|
6959
7012
|
|
|
6960
7013
|
# Write PID file for ALL modes (foreground + background)
|
package/dashboard/__init__.py
CHANGED
package/dashboard/auth.py
CHANGED
|
@@ -33,6 +33,7 @@ TOKEN_FILE = TOKEN_DIR / "tokens.json"
|
|
|
33
33
|
OIDC_ISSUER = os.environ.get("LOKI_OIDC_ISSUER", "") # e.g., https://accounts.google.com
|
|
34
34
|
OIDC_CLIENT_ID = os.environ.get("LOKI_OIDC_CLIENT_ID", "")
|
|
35
35
|
OIDC_AUDIENCE = os.environ.get("LOKI_OIDC_AUDIENCE", "") # Usually same as client_id
|
|
36
|
+
OIDC_SKIP_SIGNATURE_VERIFY = os.environ.get("LOKI_OIDC_SKIP_SIGNATURE_VERIFY", "").lower() in ("true", "1", "yes")
|
|
36
37
|
OIDC_ENABLED = bool(OIDC_ISSUER and OIDC_CLIENT_ID)
|
|
37
38
|
|
|
38
39
|
# Role-to-scope mapping (predefined roles)
|
|
@@ -54,11 +55,19 @@ _SCOPE_HIERARCHY = {
|
|
|
54
55
|
|
|
55
56
|
if OIDC_ENABLED:
|
|
56
57
|
import logging as _logging
|
|
57
|
-
_logging.getLogger("loki.auth")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
_logger = _logging.getLogger("loki.auth")
|
|
59
|
+
if OIDC_SKIP_SIGNATURE_VERIFY:
|
|
60
|
+
_logger.critical(
|
|
61
|
+
"OIDC/SSO signature verification DISABLED (LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true). "
|
|
62
|
+
"This is INSECURE and allows forged JWTs. Only use for local testing. "
|
|
63
|
+
"For production, install PyJWT + cryptography and remove this env var."
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
_logger.warning(
|
|
67
|
+
"OIDC/SSO enabled (EXPERIMENTAL). Claims-based validation only -- "
|
|
68
|
+
"JWT signatures are NOT cryptographically verified. Install PyJWT + "
|
|
69
|
+
"cryptography for production signature verification."
|
|
70
|
+
)
|
|
62
71
|
|
|
63
72
|
# OIDC JWKS cache (issuer URL -> (keys_dict, fetch_timestamp))
|
|
64
73
|
_oidc_jwks_cache = {} # type: dict[str, tuple[dict, float]]
|
|
@@ -452,15 +461,18 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
|
|
|
452
461
|
|
|
453
462
|
This is a claims-based validation that checks:
|
|
454
463
|
- Token structure (3 base64url-encoded parts)
|
|
464
|
+
- Signature part is not empty (basic sanity check)
|
|
455
465
|
- Issuer matches OIDC_ISSUER
|
|
456
466
|
- Audience matches OIDC_AUDIENCE or OIDC_CLIENT_ID
|
|
457
467
|
- Token is not expired
|
|
458
468
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
469
|
+
SECURITY WARNING: Cryptographic signature verification is NOT performed
|
|
470
|
+
unless PyJWT is installed. This implementation only validates claims.
|
|
471
|
+
An attacker can forge JWTs unless LOKI_OIDC_SKIP_SIGNATURE_VERIFY is
|
|
472
|
+
explicitly set to 'true' (which is INSECURE and only for local testing).
|
|
473
|
+
|
|
474
|
+
For production: Install PyJWT + cryptography for proper RSA/HMAC
|
|
475
|
+
signature verification.
|
|
464
476
|
"""
|
|
465
477
|
if not OIDC_ENABLED:
|
|
466
478
|
return None
|
|
@@ -470,8 +482,29 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
|
|
|
470
482
|
if len(parts) != 3:
|
|
471
483
|
return None
|
|
472
484
|
|
|
485
|
+
# Basic sanity check: signature part must not be empty
|
|
486
|
+
header_b64, payload_b64, signature_b64 = parts
|
|
487
|
+
if not signature_b64 or len(signature_b64) < 10:
|
|
488
|
+
import logging as _logging
|
|
489
|
+
_logging.getLogger("loki.auth").error(
|
|
490
|
+
"OIDC token rejected: signature part is missing or too short"
|
|
491
|
+
)
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
# CRITICAL: Check if signature verification is explicitly skipped
|
|
495
|
+
if not OIDC_SKIP_SIGNATURE_VERIFY:
|
|
496
|
+
import logging as _logging
|
|
497
|
+
_logging.getLogger("loki.auth").critical(
|
|
498
|
+
"OIDC token received but signature verification is NOT implemented. "
|
|
499
|
+
"Set LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true to explicitly allow "
|
|
500
|
+
"unverified tokens (INSECURE - local testing only), or install "
|
|
501
|
+
"PyJWT + cryptography for production signature verification. "
|
|
502
|
+
"Rejecting token for security."
|
|
503
|
+
)
|
|
504
|
+
return None
|
|
505
|
+
|
|
473
506
|
# Decode payload (claims)
|
|
474
|
-
claims = json.loads(_base64url_decode(
|
|
507
|
+
claims = json.loads(_base64url_decode(payload_b64))
|
|
475
508
|
|
|
476
509
|
# Validate issuer
|
|
477
510
|
if claims.get("iss") != OIDC_ISSUER:
|
package/dashboard/control.py
CHANGED
|
@@ -11,10 +11,12 @@ Usage:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
+
import fcntl
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import signal
|
|
17
18
|
import subprocess
|
|
19
|
+
import tempfile
|
|
18
20
|
from datetime import datetime, timezone
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Optional
|
|
@@ -49,6 +51,57 @@ RUN_SH = SKILL_DIR / "autonomy" / "run.sh"
|
|
|
49
51
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
50
52
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
53
|
|
|
54
|
+
# Utility: atomic write with optional file locking
|
|
55
|
+
def atomic_write_json(file_path: Path, data: dict, use_lock: bool = True):
|
|
56
|
+
"""
|
|
57
|
+
Atomically write JSON data to a file to prevent TOCTOU race conditions.
|
|
58
|
+
Uses temporary file + os.rename() for atomicity.
|
|
59
|
+
Optionally uses fcntl.flock for additional safety.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
# Write to temporary file in same directory (for atomic rename)
|
|
63
|
+
temp_fd, temp_path = tempfile.mkstemp(
|
|
64
|
+
dir=file_path.parent,
|
|
65
|
+
prefix=f".{file_path.name}.",
|
|
66
|
+
suffix=".tmp"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with os.fdopen(temp_fd, 'w') as f:
|
|
71
|
+
# Acquire exclusive lock if requested
|
|
72
|
+
if use_lock:
|
|
73
|
+
try:
|
|
74
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
75
|
+
except (OSError, AttributeError):
|
|
76
|
+
# flock not available on this platform - continue without lock
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Write data
|
|
80
|
+
json.dump(data, f, indent=2)
|
|
81
|
+
f.flush()
|
|
82
|
+
os.fsync(f.fileno())
|
|
83
|
+
|
|
84
|
+
# Release lock (happens automatically on close, but explicit is clearer)
|
|
85
|
+
if use_lock:
|
|
86
|
+
try:
|
|
87
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
88
|
+
except (OSError, AttributeError):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Atomic rename
|
|
92
|
+
os.rename(temp_path, file_path)
|
|
93
|
+
|
|
94
|
+
except Exception:
|
|
95
|
+
# Clean up temp file on error
|
|
96
|
+
try:
|
|
97
|
+
os.unlink(temp_path)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise RuntimeError(f"Failed to write {file_path}: {e}")
|
|
104
|
+
|
|
52
105
|
# FastAPI app
|
|
53
106
|
app = FastAPI(
|
|
54
107
|
title="Loki Mode Control API",
|
|
@@ -77,6 +130,42 @@ class StartRequest(BaseModel):
|
|
|
77
130
|
parallel: bool = False
|
|
78
131
|
background: bool = True
|
|
79
132
|
|
|
133
|
+
def validate_provider(self) -> None:
|
|
134
|
+
"""Validate provider is from allowed list."""
|
|
135
|
+
allowed_providers = ["claude", "codex", "gemini"]
|
|
136
|
+
if self.provider not in allowed_providers:
|
|
137
|
+
raise ValueError(f"Invalid provider: {self.provider}. Must be one of: {', '.join(allowed_providers)}")
|
|
138
|
+
|
|
139
|
+
def validate_prd_path(self) -> None:
|
|
140
|
+
"""Validate PRD path is safe and exists."""
|
|
141
|
+
if not self.prd:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Check for path traversal sequences
|
|
145
|
+
if ".." in self.prd:
|
|
146
|
+
raise ValueError("PRD path contains path traversal sequence (..)")
|
|
147
|
+
|
|
148
|
+
# Resolve to absolute path and verify it exists
|
|
149
|
+
prd_path = Path(self.prd).resolve()
|
|
150
|
+
if not prd_path.exists():
|
|
151
|
+
raise ValueError(f"PRD file does not exist: {self.prd}")
|
|
152
|
+
|
|
153
|
+
# Verify it's a file, not a directory
|
|
154
|
+
if not prd_path.is_file():
|
|
155
|
+
raise ValueError(f"PRD path is not a file: {self.prd}")
|
|
156
|
+
|
|
157
|
+
# Verify path resolves within CWD or a reasonable parent
|
|
158
|
+
cwd = Path.cwd().resolve()
|
|
159
|
+
try:
|
|
160
|
+
prd_path.relative_to(cwd)
|
|
161
|
+
except ValueError:
|
|
162
|
+
# Not within CWD - check if it's within user's home or project directory
|
|
163
|
+
home = Path.home().resolve()
|
|
164
|
+
try:
|
|
165
|
+
prd_path.relative_to(home)
|
|
166
|
+
except ValueError:
|
|
167
|
+
raise ValueError(f"PRD path is outside allowed directories: {self.prd}")
|
|
168
|
+
|
|
80
169
|
|
|
81
170
|
class StatusResponse(BaseModel):
|
|
82
171
|
"""Current session status."""
|
|
@@ -272,6 +361,13 @@ async def start_session(request: StartRequest):
|
|
|
272
361
|
Returns:
|
|
273
362
|
ControlResponse with success status and PID
|
|
274
363
|
"""
|
|
364
|
+
# Validate input
|
|
365
|
+
try:
|
|
366
|
+
request.validate_provider()
|
|
367
|
+
request.validate_prd_path()
|
|
368
|
+
except ValueError as e:
|
|
369
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
370
|
+
|
|
275
371
|
# Check if already running
|
|
276
372
|
status = get_status()
|
|
277
373
|
if status.state == "running":
|
|
@@ -356,10 +452,13 @@ async def stop_session():
|
|
|
356
452
|
session_file = LOKI_DIR / "session.json"
|
|
357
453
|
if session_file.exists():
|
|
358
454
|
try:
|
|
455
|
+
# Read current session data
|
|
359
456
|
session_data = json.loads(session_file.read_text())
|
|
360
457
|
session_data["status"] = "stopped"
|
|
361
|
-
|
|
362
|
-
|
|
458
|
+
|
|
459
|
+
# Atomic write with file locking to prevent race conditions
|
|
460
|
+
atomic_write_json(session_file, session_data, use_lock=True)
|
|
461
|
+
except (json.JSONDecodeError, KeyError, RuntimeError):
|
|
363
462
|
pass
|
|
364
463
|
|
|
365
464
|
# Emit stop event
|
package/dashboard/registry.py
CHANGED
|
@@ -44,7 +44,7 @@ def _save_registry(registry: dict) -> None:
|
|
|
44
44
|
|
|
45
45
|
def _generate_project_id(path: str) -> str:
|
|
46
46
|
"""Generate a unique project ID from path."""
|
|
47
|
-
return hashlib.
|
|
47
|
+
return hashlib.sha256(path.encode()).hexdigest()[:12]
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def register_project(
|
|
@@ -286,7 +286,8 @@ def discover_projects(
|
|
|
286
286
|
|
|
287
287
|
# Search subdirectories
|
|
288
288
|
for child in path.iterdir():
|
|
289
|
-
|
|
289
|
+
# Skip symlinks to avoid following into unexpected directories
|
|
290
|
+
if child.is_dir() and not child.name.startswith(".") and not child.is_symlink():
|
|
290
291
|
search_dir(child, depth + 1)
|
|
291
292
|
|
|
292
293
|
except (PermissionError, OSError):
|
package/dashboard/server.py
CHANGED
|
@@ -68,14 +68,33 @@ LOKI_TLS_KEY = os.environ.get("LOKI_TLS_KEY", "") # Path to PEM private key
|
|
|
68
68
|
class _RateLimiter:
|
|
69
69
|
"""Simple in-memory rate limiter for control endpoints."""
|
|
70
70
|
|
|
71
|
-
def __init__(self, max_calls: int = 10, window_seconds: int = 60):
|
|
71
|
+
def __init__(self, max_calls: int = 10, window_seconds: int = 60, max_keys: int = 10000):
|
|
72
72
|
self._max_calls = max_calls
|
|
73
73
|
self._window = window_seconds
|
|
74
|
+
self._max_keys = max_keys
|
|
74
75
|
self._calls: dict[str, list[float]] = defaultdict(list)
|
|
75
76
|
|
|
76
77
|
def check(self, key: str) -> bool:
|
|
77
78
|
now = time.time()
|
|
79
|
+
# Prune old timestamps for this key
|
|
78
80
|
self._calls[key] = [t for t in self._calls[key] if now - t < self._window]
|
|
81
|
+
|
|
82
|
+
# Remove keys with empty timestamp lists
|
|
83
|
+
empty_keys = [k for k, v in self._calls.items() if not v]
|
|
84
|
+
for k in empty_keys:
|
|
85
|
+
del self._calls[k]
|
|
86
|
+
|
|
87
|
+
# Evict oldest keys if max_keys exceeded
|
|
88
|
+
if len(self._calls) > self._max_keys:
|
|
89
|
+
# Sort by oldest timestamp, remove oldest keys
|
|
90
|
+
sorted_keys = sorted(
|
|
91
|
+
self._calls.items(),
|
|
92
|
+
key=lambda x: min(x[1]) if x[1] else 0
|
|
93
|
+
)
|
|
94
|
+
keys_to_remove = len(self._calls) - self._max_keys
|
|
95
|
+
for k, _ in sorted_keys[:keys_to_remove]:
|
|
96
|
+
del self._calls[k]
|
|
97
|
+
|
|
79
98
|
if len(self._calls[key]) >= self._max_calls:
|
|
80
99
|
return False
|
|
81
100
|
self._calls[key].append(now)
|
|
@@ -83,6 +102,7 @@ class _RateLimiter:
|
|
|
83
102
|
|
|
84
103
|
|
|
85
104
|
_control_limiter = _RateLimiter(max_calls=10, window_seconds=60)
|
|
105
|
+
_read_limiter = _RateLimiter(max_calls=60, window_seconds=60)
|
|
86
106
|
|
|
87
107
|
# Set up logging
|
|
88
108
|
logger = logging.getLogger(__name__)
|
|
@@ -193,11 +213,18 @@ class StatusResponse(BaseModel):
|
|
|
193
213
|
class ConnectionManager:
|
|
194
214
|
"""Manages WebSocket connections for real-time updates."""
|
|
195
215
|
|
|
216
|
+
MAX_CONNECTIONS = int(os.environ.get("LOKI_MAX_WS_CONNECTIONS", "100"))
|
|
217
|
+
|
|
196
218
|
def __init__(self):
|
|
197
219
|
self.active_connections: list[WebSocket] = []
|
|
198
220
|
|
|
199
221
|
async def connect(self, websocket: WebSocket) -> None:
|
|
200
222
|
"""Accept a new WebSocket connection."""
|
|
223
|
+
if len(self.active_connections) >= self.MAX_CONNECTIONS:
|
|
224
|
+
await websocket.accept()
|
|
225
|
+
await websocket.close(code=1013, reason="Connection limit reached. Try again later.")
|
|
226
|
+
logger.warning(f"WebSocket connection rejected: limit of {self.MAX_CONNECTIONS} reached")
|
|
227
|
+
return
|
|
201
228
|
await websocket.accept()
|
|
202
229
|
self.active_connections.append(websocket)
|
|
203
230
|
|
|
@@ -853,6 +880,13 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
853
880
|
# Authorization headers on WS upgrade. Tokens may appear in reverse
|
|
854
881
|
# proxy access logs -- configure log sanitization for /ws in production.
|
|
855
882
|
# FastAPI Depends() is not supported on @app.websocket() routes.
|
|
883
|
+
|
|
884
|
+
# Rate limit WebSocket connections by IP
|
|
885
|
+
client_ip = websocket.client.host if websocket.client else "unknown"
|
|
886
|
+
if not _read_limiter.check(f"ws_{client_ip}"):
|
|
887
|
+
await websocket.close(code=1008) # Policy Violation
|
|
888
|
+
return
|
|
889
|
+
|
|
856
890
|
if auth.is_enterprise_mode() or auth.is_oidc_mode():
|
|
857
891
|
ws_token: Optional[str] = websocket.query_params.get("token")
|
|
858
892
|
if not ws_token:
|
|
@@ -1019,8 +1053,9 @@ async def update_project_access(identifier: str):
|
|
|
1019
1053
|
|
|
1020
1054
|
|
|
1021
1055
|
@app.get("/api/registry/discover", response_model=list[DiscoverResponse])
|
|
1022
|
-
async def discover_projects(max_depth: int = 3):
|
|
1056
|
+
async def discover_projects(max_depth: int = Query(default=3, ge=1, le=10)):
|
|
1023
1057
|
"""Discover projects with .loki directories."""
|
|
1058
|
+
max_depth = min(max_depth, 10)
|
|
1024
1059
|
discovered = registry.discover_projects(max_depth=max_depth)
|
|
1025
1060
|
return discovered
|
|
1026
1061
|
|
|
@@ -1028,6 +1063,9 @@ async def discover_projects(max_depth: int = 3):
|
|
|
1028
1063
|
@app.post("/api/registry/sync", response_model=SyncResponse)
|
|
1029
1064
|
async def sync_registry():
|
|
1030
1065
|
"""Sync the registry with discovered projects."""
|
|
1066
|
+
if not _read_limiter.check("registry_sync"):
|
|
1067
|
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
1068
|
+
|
|
1031
1069
|
result = registry.sync_registry_with_discovery()
|
|
1032
1070
|
return {
|
|
1033
1071
|
"added": result["added"],
|
|
@@ -1109,6 +1147,9 @@ async def create_token(request: TokenCreateRequest):
|
|
|
1109
1147
|
|
|
1110
1148
|
The raw token is only returned once on creation - save it securely.
|
|
1111
1149
|
"""
|
|
1150
|
+
if not _read_limiter.check("token_create"):
|
|
1151
|
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
1152
|
+
|
|
1112
1153
|
if not auth.is_enterprise_mode():
|
|
1113
1154
|
raise HTTPException(
|
|
1114
1155
|
status_code=403,
|
|
@@ -1562,6 +1603,9 @@ async def get_learning_aggregation():
|
|
|
1562
1603
|
@app.post("/api/learning/aggregate", dependencies=[Depends(auth.require_scope("control"))])
|
|
1563
1604
|
async def trigger_aggregation():
|
|
1564
1605
|
"""Aggregate learning signals from events.jsonl into structured metrics."""
|
|
1606
|
+
if not _read_limiter.check("learning_aggregate"):
|
|
1607
|
+
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
1608
|
+
|
|
1565
1609
|
events_file = _get_loki_dir() / "events.jsonl"
|
|
1566
1610
|
preferences: dict = {}
|
|
1567
1611
|
error_patterns: dict = {}
|
|
@@ -1693,17 +1737,34 @@ def _parse_time_range(time_range: str) -> Optional[datetime]:
|
|
|
1693
1737
|
return datetime.now(timezone.utc) - delta
|
|
1694
1738
|
|
|
1695
1739
|
|
|
1696
|
-
def _read_events(time_range: str = "7d") -> list:
|
|
1697
|
-
"""Read events from .loki/events.jsonl with time filter."""
|
|
1740
|
+
def _read_events(time_range: str = "7d", max_events: int = 10000) -> list:
|
|
1741
|
+
"""Read events from .loki/events.jsonl with time filter and size limits."""
|
|
1698
1742
|
events_file = _get_loki_dir() / "events.jsonl"
|
|
1699
1743
|
if not events_file.exists():
|
|
1700
1744
|
return []
|
|
1701
1745
|
|
|
1702
1746
|
cutoff = _parse_time_range(time_range)
|
|
1703
1747
|
events = []
|
|
1748
|
+
max_file_size = 10 * 1024 * 1024 # 10MB
|
|
1749
|
+
|
|
1704
1750
|
try:
|
|
1705
|
-
|
|
1706
|
-
|
|
1751
|
+
file_size = events_file.stat().st_size
|
|
1752
|
+
|
|
1753
|
+
# If file > 10MB, seek to last 10MB
|
|
1754
|
+
with open(events_file, 'r') as f:
|
|
1755
|
+
if file_size > max_file_size:
|
|
1756
|
+
f.seek(max(0, file_size - max_file_size))
|
|
1757
|
+
# Skip partial first line after seek
|
|
1758
|
+
f.readline()
|
|
1759
|
+
|
|
1760
|
+
for line in f:
|
|
1761
|
+
if len(events) >= max_events:
|
|
1762
|
+
break
|
|
1763
|
+
|
|
1764
|
+
line = line.strip()
|
|
1765
|
+
if not line:
|
|
1766
|
+
continue
|
|
1767
|
+
|
|
1707
1768
|
try:
|
|
1708
1769
|
event = json.loads(line)
|
|
1709
1770
|
# Filter by time_range if cutoff was parsed successfully
|
|
@@ -651,7 +651,7 @@
|
|
|
651
651
|
|
|
652
652
|
<!-- Inlined JavaScript Bundle -->
|
|
653
653
|
<script>
|
|
654
|
-
var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var he=(d,e,t)=>e in d?K(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var ve=(d,e)=>{for(var t in e)K(d,t,{get:e[t],enumerable:!0})},me=(d,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ue(e))!ge.call(d,i)&&i!==t&&K(d,i,{get:()=>e[i],enumerable:!(a=pe(e,i))||a.enumerable});return d};var be=d=>me(K({},"__esModule",{value:!0}),d);var k=(d,e,t)=>he(d,typeof e!="symbol"?e+"":e,t);var Se={};ve(Se,{ANIMATION:()=>w,ARIA_PATTERNS:()=>W,ApiEvents:()=>n,BASE_STYLES:()=>R,BREAKPOINTS:()=>V,COMMON_STYLES:()=>te,KEYBOARD_SHORTCUTS:()=>Y,KeyboardHandler:()=>L,LokiApiClient:()=>I,LokiCheckpointViewer:()=>q,LokiContextTracker:()=>G,LokiCostDashboard:()=>N,LokiCouncilDashboard:()=>O,LokiElement:()=>c,LokiLearningDashboard:()=>F,LokiLogStream:()=>U,LokiMemoryBrowser:()=>j,LokiNotificationCenter:()=>J,LokiOverview:()=>z,LokiSessionControl:()=>H,LokiState:()=>M,LokiTaskBoard:()=>B,LokiTheme:()=>S,RADIUS:()=>y,SPACING:()=>x,STATE_CHANGE_EVENT:()=>Q,THEMES:()=>b,THEME_VARIABLES:()=>X,TYPOGRAPHY:()=>v,UnifiedThemeManager:()=>h,VERSION:()=>Te,Z_INDEX:()=>$,createApiClient:()=>se,createStore:()=>re,generateThemeCSS:()=>m,generateTokensCSS:()=>P,getApiClient:()=>u,getState:()=>C,init:()=>Ee});var b={light:{"--loki-bg-primary":"#fafafa","--loki-bg-secondary":"#f4f4f5","--loki-bg-tertiary":"#e4e4e7","--loki-bg-card":"#ffffff","--loki-bg-hover":"#f0f0f3","--loki-bg-active":"#e8e8ec","--loki-bg-overlay":"rgba(0, 0, 0, 0.5)","--loki-accent":"#7c3aed","--loki-accent-hover":"#6d28d9","--loki-accent-active":"#5b21b6","--loki-accent-light":"#8b5cf6","--loki-accent-muted":"rgba(124, 58, 237, 0.12)","--loki-text-primary":"#18181b","--loki-text-secondary":"#52525b","--loki-text-muted":"#a1a1aa","--loki-text-disabled":"#d4d4d8","--loki-text-inverse":"#ffffff","--loki-border":"#e4e4e7","--loki-border-light":"#d4d4d8","--loki-border-focus":"#7c3aed","--loki-success":"#16a34a","--loki-success-muted":"rgba(22, 163, 74, 0.12)","--loki-warning":"#ca8a04","--loki-warning-muted":"rgba(202, 138, 4, 0.12)","--loki-error":"#dc2626","--loki-error-muted":"rgba(220, 38, 38, 0.12)","--loki-info":"#2563eb","--loki-info-muted":"rgba(37, 99, 235, 0.12)","--loki-green":"#16a34a","--loki-green-muted":"rgba(22, 163, 74, 0.12)","--loki-yellow":"#ca8a04","--loki-yellow-muted":"rgba(202, 138, 4, 0.12)","--loki-red":"#dc2626","--loki-red-muted":"rgba(220, 38, 38, 0.12)","--loki-blue":"#2563eb","--loki-blue-muted":"rgba(37, 99, 235, 0.12)","--loki-purple":"#9333ea","--loki-purple-muted":"rgba(147, 51, 234, 0.12)","--loki-opus":"#d97706","--loki-sonnet":"#4f46e5","--loki-haiku":"#059669","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.05)","--loki-shadow-md":"0 4px 6px rgba(0, 0, 0, 0.07)","--loki-shadow-lg":"0 10px 15px rgba(0, 0, 0, 0.1)","--loki-shadow-focus":"0 0 0 3px rgba(124, 58, 237, 0.3)"},dark:{"--loki-bg-primary":"#09090b","--loki-bg-secondary":"#0c0c0f","--loki-bg-tertiary":"#111114","--loki-bg-card":"#18181b","--loki-bg-hover":"#1f1f23","--loki-bg-active":"#27272a","--loki-bg-overlay":"rgba(0, 0, 0, 0.8)","--loki-accent":"#8b5cf6","--loki-accent-hover":"#a78bfa","--loki-accent-active":"#7c3aed","--loki-accent-light":"#a78bfa","--loki-accent-muted":"rgba(139, 92, 246, 0.15)","--loki-text-primary":"#fafafa","--loki-text-secondary":"#a1a1aa","--loki-text-muted":"#52525b","--loki-text-disabled":"#3f3f46","--loki-text-inverse":"#09090b","--loki-border":"rgba(255, 255, 255, 0.06)","--loki-border-light":"rgba(255, 255, 255, 0.1)","--loki-border-focus":"#8b5cf6","--loki-success":"#22c55e","--loki-success-muted":"rgba(34, 197, 94, 0.15)","--loki-warning":"#eab308","--loki-warning-muted":"rgba(234, 179, 8, 0.15)","--loki-error":"#ef4444","--loki-error-muted":"rgba(239, 68, 68, 0.15)","--loki-info":"#3b82f6","--loki-info-muted":"rgba(59, 130, 246, 0.15)","--loki-green":"#22c55e","--loki-green-muted":"rgba(34, 197, 94, 0.15)","--loki-yellow":"#eab308","--loki-yellow-muted":"rgba(234, 179, 8, 0.15)","--loki-red":"#ef4444","--loki-red-muted":"rgba(239, 68, 68, 0.15)","--loki-blue":"#3b82f6","--loki-blue-muted":"rgba(59, 130, 246, 0.15)","--loki-purple":"#a78bfa","--loki-purple-muted":"rgba(167, 139, 250, 0.15)","--loki-opus":"#f59e0b","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.4)","--loki-shadow-md":"0 4px 12px rgba(0, 0, 0, 0.5)","--loki-shadow-lg":"0 10px 25px rgba(0, 0, 0, 0.6)","--loki-shadow-focus":"0 0 0 3px rgba(139, 92, 246, 0.25)"},"high-contrast":{"--loki-bg-primary":"#000000","--loki-bg-secondary":"#0a0a0a","--loki-bg-tertiary":"#141414","--loki-bg-card":"#0a0a0a","--loki-bg-hover":"#1a1a1a","--loki-bg-active":"#242424","--loki-bg-overlay":"rgba(0, 0, 0, 0.9)","--loki-accent":"#c084fc","--loki-accent-hover":"#d8b4fe","--loki-accent-active":"#e9d5ff","--loki-accent-light":"#d8b4fe","--loki-accent-muted":"rgba(192, 132, 252, 0.25)","--loki-text-primary":"#ffffff","--loki-text-secondary":"#e0e0e0","--loki-text-muted":"#b0b0b0","--loki-text-disabled":"#666666","--loki-text-inverse":"#000000","--loki-border":"#ffffff","--loki-border-light":"#cccccc","--loki-border-focus":"#c084fc","--loki-success":"#4ade80","--loki-success-muted":"rgba(74, 222, 128, 0.25)","--loki-warning":"#fde047","--loki-warning-muted":"rgba(253, 224, 71, 0.25)","--loki-error":"#f87171","--loki-error-muted":"rgba(248, 113, 113, 0.25)","--loki-info":"#60a5fa","--loki-info-muted":"rgba(96, 165, 250, 0.25)","--loki-green":"#4ade80","--loki-green-muted":"rgba(74, 222, 128, 0.25)","--loki-yellow":"#fde047","--loki-yellow-muted":"rgba(253, 224, 71, 0.25)","--loki-red":"#f87171","--loki-red-muted":"rgba(248, 113, 113, 0.25)","--loki-blue":"#60a5fa","--loki-blue-muted":"rgba(96, 165, 250, 0.25)","--loki-purple":"#c084fc","--loki-purple-muted":"rgba(192, 132, 252, 0.25)","--loki-opus":"#fbbf24","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"none","--loki-shadow-md":"none","--loki-shadow-lg":"none","--loki-shadow-focus":"0 0 0 3px #c084fc"},"vscode-light":{"--loki-bg-primary":"var(--vscode-editor-background, #ffffff)","--loki-bg-secondary":"var(--vscode-sideBar-background, #f3f3f3)","--loki-bg-tertiary":"var(--vscode-input-background, #ffffff)","--loki-bg-card":"var(--vscode-editor-background, #ffffff)","--loki-bg-hover":"var(--vscode-list-hoverBackground, #e8e8e8)","--loki-bg-active":"var(--vscode-list-activeSelectionBackground, #0060c0)","--loki-bg-overlay":"rgba(0, 0, 0, 0.4)","--loki-accent":"var(--vscode-focusBorder, #0066cc)","--loki-accent-hover":"var(--vscode-button-hoverBackground, #0055aa)","--loki-accent-active":"var(--vscode-button-background, #007acc)","--loki-accent-light":"var(--vscode-focusBorder, #0066cc)","--loki-accent-muted":"var(--vscode-editor-selectionBackground, rgba(0, 102, 204, 0.2))","--loki-text-primary":"var(--vscode-foreground, #333333)","--loki-text-secondary":"var(--vscode-descriptionForeground, #717171)","--loki-text-muted":"var(--vscode-disabledForeground, #a0a0a0)","--loki-text-disabled":"var(--vscode-disabledForeground, #cccccc)","--loki-text-inverse":"var(--vscode-button-foreground, #ffffff)","--loki-border":"var(--vscode-widget-border, #c8c8c8)","--loki-border-light":"var(--vscode-widget-border, #e0e0e0)","--loki-border-focus":"var(--vscode-focusBorder, #0066cc)","--loki-success":"var(--vscode-testing-iconPassed, #388a34)","--loki-success-muted":"rgba(56, 138, 52, 0.15)","--loki-warning":"var(--vscode-editorWarning-foreground, #bf8803)","--loki-warning-muted":"rgba(191, 136, 3, 0.15)","--loki-error":"var(--vscode-errorForeground, #e51400)","--loki-error-muted":"rgba(229, 20, 0, 0.15)","--loki-info":"var(--vscode-editorInfo-foreground, #1a85ff)","--loki-info-muted":"rgba(26, 133, 255, 0.15)","--loki-green":"var(--vscode-testing-iconPassed, #388a34)","--loki-green-muted":"rgba(56, 138, 52, 0.15)","--loki-yellow":"var(--vscode-editorWarning-foreground, #bf8803)","--loki-yellow-muted":"rgba(191, 136, 3, 0.15)","--loki-red":"var(--vscode-errorForeground, #e51400)","--loki-red-muted":"rgba(229, 20, 0, 0.15)","--loki-blue":"var(--vscode-editorInfo-foreground, #1a85ff)","--loki-blue-muted":"rgba(26, 133, 255, 0.15)","--loki-purple":"#9333ea","--loki-purple-muted":"rgba(147, 51, 234, 0.15)","--loki-opus":"#d97706","--loki-sonnet":"#4f46e5","--loki-haiku":"#059669","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.05)","--loki-shadow-md":"0 2px 4px rgba(0, 0, 0, 0.1)","--loki-shadow-lg":"0 4px 8px rgba(0, 0, 0, 0.15)","--loki-shadow-focus":"0 0 0 2px var(--vscode-focusBorder, #0066cc)"},"vscode-dark":{"--loki-bg-primary":"var(--vscode-editor-background, #1e1e1e)","--loki-bg-secondary":"var(--vscode-sideBar-background, #252526)","--loki-bg-tertiary":"var(--vscode-input-background, #3c3c3c)","--loki-bg-card":"var(--vscode-editor-background, #1e1e1e)","--loki-bg-hover":"var(--vscode-list-hoverBackground, #2a2d2e)","--loki-bg-active":"var(--vscode-list-activeSelectionBackground, #094771)","--loki-bg-overlay":"rgba(0, 0, 0, 0.6)","--loki-accent":"var(--vscode-focusBorder, #007fd4)","--loki-accent-hover":"var(--vscode-button-hoverBackground, #1177bb)","--loki-accent-active":"var(--vscode-button-background, #0e639c)","--loki-accent-light":"var(--vscode-focusBorder, #007fd4)","--loki-accent-muted":"var(--vscode-editor-selectionBackground, rgba(0, 127, 212, 0.25))","--loki-text-primary":"var(--vscode-foreground, #cccccc)","--loki-text-secondary":"var(--vscode-descriptionForeground, #9d9d9d)","--loki-text-muted":"var(--vscode-disabledForeground, #6b6b6b)","--loki-text-disabled":"var(--vscode-disabledForeground, #4d4d4d)","--loki-text-inverse":"var(--vscode-button-foreground, #ffffff)","--loki-border":"var(--vscode-widget-border, #454545)","--loki-border-light":"var(--vscode-widget-border, #5a5a5a)","--loki-border-focus":"var(--vscode-focusBorder, #007fd4)","--loki-success":"var(--vscode-testing-iconPassed, #89d185)","--loki-success-muted":"rgba(137, 209, 133, 0.2)","--loki-warning":"var(--vscode-editorWarning-foreground, #cca700)","--loki-warning-muted":"rgba(204, 167, 0, 0.2)","--loki-error":"var(--vscode-errorForeground, #f48771)","--loki-error-muted":"rgba(244, 135, 113, 0.2)","--loki-info":"var(--vscode-editorInfo-foreground, #75beff)","--loki-info-muted":"rgba(117, 190, 255, 0.2)","--loki-green":"var(--vscode-testing-iconPassed, #89d185)","--loki-green-muted":"rgba(137, 209, 133, 0.2)","--loki-yellow":"var(--vscode-editorWarning-foreground, #cca700)","--loki-yellow-muted":"rgba(204, 167, 0, 0.2)","--loki-red":"var(--vscode-errorForeground, #f48771)","--loki-red-muted":"rgba(244, 135, 113, 0.2)","--loki-blue":"var(--vscode-editorInfo-foreground, #75beff)","--loki-blue-muted":"rgba(117, 190, 255, 0.2)","--loki-purple":"#c084fc","--loki-purple-muted":"rgba(192, 132, 252, 0.2)","--loki-opus":"#f59e0b","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.3)","--loki-shadow-md":"0 2px 4px rgba(0, 0, 0, 0.4)","--loki-shadow-lg":"0 4px 8px rgba(0, 0, 0, 0.5)","--loki-shadow-focus":"0 0 0 2px var(--vscode-focusBorder, #007fd4)"}},x={xs:"4px",sm:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"32px","3xl":"48px"},y={none:"0",sm:"4px",md:"6px",lg:"8px",xl:"10px",full:"9999px"},v={fontFamily:{sans:"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",mono:"'JetBrains Mono', 'Fira Code', 'SF Mono', Menlo, monospace"},fontSize:{xs:"10px",sm:"11px",base:"12px",md:"13px",lg:"14px",xl:"16px","2xl":"18px","3xl":"24px"},fontWeight:{normal:"400",medium:"500",semibold:"600",bold:"700"},lineHeight:{tight:"1.25",normal:"1.5",relaxed:"1.75"}},w={duration:{fast:"100ms",normal:"200ms",slow:"300ms",slower:"500ms"},easing:{default:"cubic-bezier(0.4, 0, 0.2, 1)",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)",bounce:"cubic-bezier(0.68, -0.55, 0.265, 1.55)"}},V={sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},$={base:"0",dropdown:"100",sticky:"200",modal:"300",popover:"400",tooltip:"500",toast:"600"},Y={"navigation.nextItem":{key:"ArrowDown",modifiers:[]},"navigation.prevItem":{key:"ArrowUp",modifiers:[]},"navigation.nextSection":{key:"Tab",modifiers:[]},"navigation.prevSection":{key:"Tab",modifiers:["Shift"]},"navigation.confirm":{key:"Enter",modifiers:[]},"navigation.cancel":{key:"Escape",modifiers:[]},"action.refresh":{key:"r",modifiers:["Meta"]},"action.search":{key:"k",modifiers:["Meta"]},"action.save":{key:"s",modifiers:["Meta"]},"action.close":{key:"w",modifiers:["Meta"]},"theme.toggle":{key:"d",modifiers:["Meta","Shift"]},"task.create":{key:"n",modifiers:["Meta"]},"task.complete":{key:"Enter",modifiers:["Meta"]},"view.toggleLogs":{key:"l",modifiers:["Meta","Shift"]},"view.toggleMemory":{key:"m",modifiers:["Meta","Shift"]}},W={button:{role:"button",tabIndex:0},tablist:{role:"tablist"},tab:{role:"tab",ariaSelected:!1,tabIndex:-1},tabpanel:{role:"tabpanel",tabIndex:0},list:{role:"list"},listitem:{role:"listitem"},livePolite:{ariaLive:"polite",ariaAtomic:!0},liveAssertive:{ariaLive:"assertive",ariaAtomic:!0},dialog:{role:"dialog",ariaModal:!0},alertdialog:{role:"alertdialog",ariaModal:!0},status:{role:"status",ariaLive:"polite"},alert:{role:"alert",ariaLive:"assertive"},log:{role:"log",ariaLive:"polite",ariaRelevant:"additions"}};function m(d){let e=b[d];return e?Object.entries(e).map(([t,a])=>`${t}: ${a};`).join(`
|
|
654
|
+
var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var he=(d,e,t)=>e in d?K(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var ve=(d,e)=>{for(var t in e)K(d,t,{get:e[t],enumerable:!0})},me=(d,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ue(e))!ge.call(d,i)&&i!==t&&K(d,i,{get:()=>e[i],enumerable:!(a=pe(e,i))||a.enumerable});return d};var be=d=>me(K({},"__esModule",{value:!0}),d);var k=(d,e,t)=>he(d,typeof e!="symbol"?e+"":e,t);var Se={};ve(Se,{ANIMATION:()=>w,ARIA_PATTERNS:()=>W,ApiEvents:()=>n,BASE_STYLES:()=>M,BREAKPOINTS:()=>V,COMMON_STYLES:()=>te,KEYBOARD_SHORTCUTS:()=>Y,KeyboardHandler:()=>L,LokiApiClient:()=>I,LokiCheckpointViewer:()=>q,LokiContextTracker:()=>G,LokiCostDashboard:()=>N,LokiCouncilDashboard:()=>O,LokiElement:()=>c,LokiLearningDashboard:()=>F,LokiLogStream:()=>U,LokiMemoryBrowser:()=>j,LokiNotificationCenter:()=>J,LokiOverview:()=>z,LokiSessionControl:()=>B,LokiState:()=>R,LokiTaskBoard:()=>H,LokiTheme:()=>S,RADIUS:()=>y,SPACING:()=>x,STATE_CHANGE_EVENT:()=>Q,THEMES:()=>b,THEME_VARIABLES:()=>X,TYPOGRAPHY:()=>v,UnifiedThemeManager:()=>h,VERSION:()=>Te,Z_INDEX:()=>$,createApiClient:()=>se,createStore:()=>re,generateThemeCSS:()=>m,generateTokensCSS:()=>P,getApiClient:()=>u,getState:()=>C,init:()=>Ee});var b={light:{"--loki-bg-primary":"#fafafa","--loki-bg-secondary":"#f4f4f5","--loki-bg-tertiary":"#e4e4e7","--loki-bg-card":"#ffffff","--loki-bg-hover":"#f0f0f3","--loki-bg-active":"#e8e8ec","--loki-bg-overlay":"rgba(0, 0, 0, 0.5)","--loki-accent":"#7c3aed","--loki-accent-hover":"#6d28d9","--loki-accent-active":"#5b21b6","--loki-accent-light":"#8b5cf6","--loki-accent-muted":"rgba(124, 58, 237, 0.12)","--loki-text-primary":"#18181b","--loki-text-secondary":"#52525b","--loki-text-muted":"#a1a1aa","--loki-text-disabled":"#d4d4d8","--loki-text-inverse":"#ffffff","--loki-border":"#e4e4e7","--loki-border-light":"#d4d4d8","--loki-border-focus":"#7c3aed","--loki-success":"#16a34a","--loki-success-muted":"rgba(22, 163, 74, 0.12)","--loki-warning":"#ca8a04","--loki-warning-muted":"rgba(202, 138, 4, 0.12)","--loki-error":"#dc2626","--loki-error-muted":"rgba(220, 38, 38, 0.12)","--loki-info":"#2563eb","--loki-info-muted":"rgba(37, 99, 235, 0.12)","--loki-green":"#16a34a","--loki-green-muted":"rgba(22, 163, 74, 0.12)","--loki-yellow":"#ca8a04","--loki-yellow-muted":"rgba(202, 138, 4, 0.12)","--loki-red":"#dc2626","--loki-red-muted":"rgba(220, 38, 38, 0.12)","--loki-blue":"#2563eb","--loki-blue-muted":"rgba(37, 99, 235, 0.12)","--loki-purple":"#9333ea","--loki-purple-muted":"rgba(147, 51, 234, 0.12)","--loki-opus":"#d97706","--loki-sonnet":"#4f46e5","--loki-haiku":"#059669","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.05)","--loki-shadow-md":"0 4px 6px rgba(0, 0, 0, 0.07)","--loki-shadow-lg":"0 10px 15px rgba(0, 0, 0, 0.1)","--loki-shadow-focus":"0 0 0 3px rgba(124, 58, 237, 0.3)"},dark:{"--loki-bg-primary":"#09090b","--loki-bg-secondary":"#0c0c0f","--loki-bg-tertiary":"#111114","--loki-bg-card":"#18181b","--loki-bg-hover":"#1f1f23","--loki-bg-active":"#27272a","--loki-bg-overlay":"rgba(0, 0, 0, 0.8)","--loki-accent":"#8b5cf6","--loki-accent-hover":"#a78bfa","--loki-accent-active":"#7c3aed","--loki-accent-light":"#a78bfa","--loki-accent-muted":"rgba(139, 92, 246, 0.15)","--loki-text-primary":"#fafafa","--loki-text-secondary":"#a1a1aa","--loki-text-muted":"#52525b","--loki-text-disabled":"#3f3f46","--loki-text-inverse":"#09090b","--loki-border":"rgba(255, 255, 255, 0.06)","--loki-border-light":"rgba(255, 255, 255, 0.1)","--loki-border-focus":"#8b5cf6","--loki-success":"#22c55e","--loki-success-muted":"rgba(34, 197, 94, 0.15)","--loki-warning":"#eab308","--loki-warning-muted":"rgba(234, 179, 8, 0.15)","--loki-error":"#ef4444","--loki-error-muted":"rgba(239, 68, 68, 0.15)","--loki-info":"#3b82f6","--loki-info-muted":"rgba(59, 130, 246, 0.15)","--loki-green":"#22c55e","--loki-green-muted":"rgba(34, 197, 94, 0.15)","--loki-yellow":"#eab308","--loki-yellow-muted":"rgba(234, 179, 8, 0.15)","--loki-red":"#ef4444","--loki-red-muted":"rgba(239, 68, 68, 0.15)","--loki-blue":"#3b82f6","--loki-blue-muted":"rgba(59, 130, 246, 0.15)","--loki-purple":"#a78bfa","--loki-purple-muted":"rgba(167, 139, 250, 0.15)","--loki-opus":"#f59e0b","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.4)","--loki-shadow-md":"0 4px 12px rgba(0, 0, 0, 0.5)","--loki-shadow-lg":"0 10px 25px rgba(0, 0, 0, 0.6)","--loki-shadow-focus":"0 0 0 3px rgba(139, 92, 246, 0.25)"},"high-contrast":{"--loki-bg-primary":"#000000","--loki-bg-secondary":"#0a0a0a","--loki-bg-tertiary":"#141414","--loki-bg-card":"#0a0a0a","--loki-bg-hover":"#1a1a1a","--loki-bg-active":"#242424","--loki-bg-overlay":"rgba(0, 0, 0, 0.9)","--loki-accent":"#c084fc","--loki-accent-hover":"#d8b4fe","--loki-accent-active":"#e9d5ff","--loki-accent-light":"#d8b4fe","--loki-accent-muted":"rgba(192, 132, 252, 0.25)","--loki-text-primary":"#ffffff","--loki-text-secondary":"#e0e0e0","--loki-text-muted":"#b0b0b0","--loki-text-disabled":"#666666","--loki-text-inverse":"#000000","--loki-border":"#ffffff","--loki-border-light":"#cccccc","--loki-border-focus":"#c084fc","--loki-success":"#4ade80","--loki-success-muted":"rgba(74, 222, 128, 0.25)","--loki-warning":"#fde047","--loki-warning-muted":"rgba(253, 224, 71, 0.25)","--loki-error":"#f87171","--loki-error-muted":"rgba(248, 113, 113, 0.25)","--loki-info":"#60a5fa","--loki-info-muted":"rgba(96, 165, 250, 0.25)","--loki-green":"#4ade80","--loki-green-muted":"rgba(74, 222, 128, 0.25)","--loki-yellow":"#fde047","--loki-yellow-muted":"rgba(253, 224, 71, 0.25)","--loki-red":"#f87171","--loki-red-muted":"rgba(248, 113, 113, 0.25)","--loki-blue":"#60a5fa","--loki-blue-muted":"rgba(96, 165, 250, 0.25)","--loki-purple":"#c084fc","--loki-purple-muted":"rgba(192, 132, 252, 0.25)","--loki-opus":"#fbbf24","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"none","--loki-shadow-md":"none","--loki-shadow-lg":"none","--loki-shadow-focus":"0 0 0 3px #c084fc"},"vscode-light":{"--loki-bg-primary":"var(--vscode-editor-background, #ffffff)","--loki-bg-secondary":"var(--vscode-sideBar-background, #f3f3f3)","--loki-bg-tertiary":"var(--vscode-input-background, #ffffff)","--loki-bg-card":"var(--vscode-editor-background, #ffffff)","--loki-bg-hover":"var(--vscode-list-hoverBackground, #e8e8e8)","--loki-bg-active":"var(--vscode-list-activeSelectionBackground, #0060c0)","--loki-bg-overlay":"rgba(0, 0, 0, 0.4)","--loki-accent":"var(--vscode-focusBorder, #0066cc)","--loki-accent-hover":"var(--vscode-button-hoverBackground, #0055aa)","--loki-accent-active":"var(--vscode-button-background, #007acc)","--loki-accent-light":"var(--vscode-focusBorder, #0066cc)","--loki-accent-muted":"var(--vscode-editor-selectionBackground, rgba(0, 102, 204, 0.2))","--loki-text-primary":"var(--vscode-foreground, #333333)","--loki-text-secondary":"var(--vscode-descriptionForeground, #717171)","--loki-text-muted":"var(--vscode-disabledForeground, #a0a0a0)","--loki-text-disabled":"var(--vscode-disabledForeground, #cccccc)","--loki-text-inverse":"var(--vscode-button-foreground, #ffffff)","--loki-border":"var(--vscode-widget-border, #c8c8c8)","--loki-border-light":"var(--vscode-widget-border, #e0e0e0)","--loki-border-focus":"var(--vscode-focusBorder, #0066cc)","--loki-success":"var(--vscode-testing-iconPassed, #388a34)","--loki-success-muted":"rgba(56, 138, 52, 0.15)","--loki-warning":"var(--vscode-editorWarning-foreground, #bf8803)","--loki-warning-muted":"rgba(191, 136, 3, 0.15)","--loki-error":"var(--vscode-errorForeground, #e51400)","--loki-error-muted":"rgba(229, 20, 0, 0.15)","--loki-info":"var(--vscode-editorInfo-foreground, #1a85ff)","--loki-info-muted":"rgba(26, 133, 255, 0.15)","--loki-green":"var(--vscode-testing-iconPassed, #388a34)","--loki-green-muted":"rgba(56, 138, 52, 0.15)","--loki-yellow":"var(--vscode-editorWarning-foreground, #bf8803)","--loki-yellow-muted":"rgba(191, 136, 3, 0.15)","--loki-red":"var(--vscode-errorForeground, #e51400)","--loki-red-muted":"rgba(229, 20, 0, 0.15)","--loki-blue":"var(--vscode-editorInfo-foreground, #1a85ff)","--loki-blue-muted":"rgba(26, 133, 255, 0.15)","--loki-purple":"#9333ea","--loki-purple-muted":"rgba(147, 51, 234, 0.15)","--loki-opus":"#d97706","--loki-sonnet":"#4f46e5","--loki-haiku":"#059669","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.05)","--loki-shadow-md":"0 2px 4px rgba(0, 0, 0, 0.1)","--loki-shadow-lg":"0 4px 8px rgba(0, 0, 0, 0.15)","--loki-shadow-focus":"0 0 0 2px var(--vscode-focusBorder, #0066cc)"},"vscode-dark":{"--loki-bg-primary":"var(--vscode-editor-background, #1e1e1e)","--loki-bg-secondary":"var(--vscode-sideBar-background, #252526)","--loki-bg-tertiary":"var(--vscode-input-background, #3c3c3c)","--loki-bg-card":"var(--vscode-editor-background, #1e1e1e)","--loki-bg-hover":"var(--vscode-list-hoverBackground, #2a2d2e)","--loki-bg-active":"var(--vscode-list-activeSelectionBackground, #094771)","--loki-bg-overlay":"rgba(0, 0, 0, 0.6)","--loki-accent":"var(--vscode-focusBorder, #007fd4)","--loki-accent-hover":"var(--vscode-button-hoverBackground, #1177bb)","--loki-accent-active":"var(--vscode-button-background, #0e639c)","--loki-accent-light":"var(--vscode-focusBorder, #007fd4)","--loki-accent-muted":"var(--vscode-editor-selectionBackground, rgba(0, 127, 212, 0.25))","--loki-text-primary":"var(--vscode-foreground, #cccccc)","--loki-text-secondary":"var(--vscode-descriptionForeground, #9d9d9d)","--loki-text-muted":"var(--vscode-disabledForeground, #6b6b6b)","--loki-text-disabled":"var(--vscode-disabledForeground, #4d4d4d)","--loki-text-inverse":"var(--vscode-button-foreground, #ffffff)","--loki-border":"var(--vscode-widget-border, #454545)","--loki-border-light":"var(--vscode-widget-border, #5a5a5a)","--loki-border-focus":"var(--vscode-focusBorder, #007fd4)","--loki-success":"var(--vscode-testing-iconPassed, #89d185)","--loki-success-muted":"rgba(137, 209, 133, 0.2)","--loki-warning":"var(--vscode-editorWarning-foreground, #cca700)","--loki-warning-muted":"rgba(204, 167, 0, 0.2)","--loki-error":"var(--vscode-errorForeground, #f48771)","--loki-error-muted":"rgba(244, 135, 113, 0.2)","--loki-info":"var(--vscode-editorInfo-foreground, #75beff)","--loki-info-muted":"rgba(117, 190, 255, 0.2)","--loki-green":"var(--vscode-testing-iconPassed, #89d185)","--loki-green-muted":"rgba(137, 209, 133, 0.2)","--loki-yellow":"var(--vscode-editorWarning-foreground, #cca700)","--loki-yellow-muted":"rgba(204, 167, 0, 0.2)","--loki-red":"var(--vscode-errorForeground, #f48771)","--loki-red-muted":"rgba(244, 135, 113, 0.2)","--loki-blue":"var(--vscode-editorInfo-foreground, #75beff)","--loki-blue-muted":"rgba(117, 190, 255, 0.2)","--loki-purple":"#c084fc","--loki-purple-muted":"rgba(192, 132, 252, 0.2)","--loki-opus":"#f59e0b","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-shadow-sm":"0 1px 2px rgba(0, 0, 0, 0.3)","--loki-shadow-md":"0 2px 4px rgba(0, 0, 0, 0.4)","--loki-shadow-lg":"0 4px 8px rgba(0, 0, 0, 0.5)","--loki-shadow-focus":"0 0 0 2px var(--vscode-focusBorder, #007fd4)"}},x={xs:"4px",sm:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"32px","3xl":"48px"},y={none:"0",sm:"4px",md:"6px",lg:"8px",xl:"10px",full:"9999px"},v={fontFamily:{sans:"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",mono:"'JetBrains Mono', 'Fira Code', 'SF Mono', Menlo, monospace"},fontSize:{xs:"10px",sm:"11px",base:"12px",md:"13px",lg:"14px",xl:"16px","2xl":"18px","3xl":"24px"},fontWeight:{normal:"400",medium:"500",semibold:"600",bold:"700"},lineHeight:{tight:"1.25",normal:"1.5",relaxed:"1.75"}},w={duration:{fast:"100ms",normal:"200ms",slow:"300ms",slower:"500ms"},easing:{default:"cubic-bezier(0.4, 0, 0.2, 1)",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)",bounce:"cubic-bezier(0.68, -0.55, 0.265, 1.55)"}},V={sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},$={base:"0",dropdown:"100",sticky:"200",modal:"300",popover:"400",tooltip:"500",toast:"600"},Y={"navigation.nextItem":{key:"ArrowDown",modifiers:[]},"navigation.prevItem":{key:"ArrowUp",modifiers:[]},"navigation.nextSection":{key:"Tab",modifiers:[]},"navigation.prevSection":{key:"Tab",modifiers:["Shift"]},"navigation.confirm":{key:"Enter",modifiers:[]},"navigation.cancel":{key:"Escape",modifiers:[]},"action.refresh":{key:"r",modifiers:["Meta"]},"action.search":{key:"k",modifiers:["Meta"]},"action.save":{key:"s",modifiers:["Meta"]},"action.close":{key:"w",modifiers:["Meta"]},"theme.toggle":{key:"d",modifiers:["Meta","Shift"]},"task.create":{key:"n",modifiers:["Meta"]},"task.complete":{key:"Enter",modifiers:["Meta"]},"view.toggleLogs":{key:"l",modifiers:["Meta","Shift"]},"view.toggleMemory":{key:"m",modifiers:["Meta","Shift"]}},W={button:{role:"button",tabIndex:0},tablist:{role:"tablist"},tab:{role:"tab",ariaSelected:!1,tabIndex:-1},tabpanel:{role:"tabpanel",tabIndex:0},list:{role:"list"},listitem:{role:"listitem"},livePolite:{ariaLive:"polite",ariaAtomic:!0},liveAssertive:{ariaLive:"assertive",ariaAtomic:!0},dialog:{role:"dialog",ariaModal:!0},alertdialog:{role:"alertdialog",ariaModal:!0},status:{role:"status",ariaLive:"polite"},alert:{role:"alert",ariaLive:"assertive"},log:{role:"log",ariaLive:"polite",ariaRelevant:"additions"}};function m(d){let e=b[d];return e?Object.entries(e).map(([t,a])=>`${t}: ${a};`).join(`
|
|
655
655
|
`):""}function P(){return`
|
|
656
656
|
/* Spacing */
|
|
657
657
|
--loki-space-xs: ${x.xs};
|
|
@@ -701,7 +701,7 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
701
701
|
--loki-glass-bg: rgba(255, 255, 255, 0.03);
|
|
702
702
|
--loki-glass-border: rgba(255, 255, 255, 0.06);
|
|
703
703
|
--loki-glass-blur: blur(12px);
|
|
704
|
-
`}var
|
|
704
|
+
`}var M=`
|
|
705
705
|
/* Reset and base */
|
|
706
706
|
:host {
|
|
707
707
|
font-family: var(--loki-font-sans);
|
|
@@ -989,7 +989,7 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
989
989
|
${m(t)}
|
|
990
990
|
${P()}
|
|
991
991
|
}
|
|
992
|
-
${
|
|
992
|
+
${M}
|
|
993
993
|
`}static init(){let e=g.getTheme();document.documentElement.setAttribute("data-loki-theme",e),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{localStorage.getItem(g.STORAGE_KEY)||g.setTheme(g.getTheme())}),g.detectContext()==="vscode"&&new MutationObserver(()=>{let a=g.getTheme();document.documentElement.setAttribute("data-loki-theme",a),window.dispatchEvent(new CustomEvent("loki-theme-change",{detail:{theme:a,context:"vscode"}}))}).observe(document.body,{attributes:!0,attributeFilter:["class"]})}};k(g,"STORAGE_KEY","loki-theme"),k(g,"CONTEXT_KEY","loki-context");var h=g,L=class{constructor(){this._handlers=new Map,this._enabled=!0}register(e,t){let a=Y[e];if(!a){console.warn(`Unknown keyboard action: ${e}`);return}this._handlers.set(e,{shortcut:a,handler:t})}unregister(e){this._handlers.delete(e)}setEnabled(e){this._enabled=e}handleEvent(e){if(!this._enabled)return!1;for(let[t,{shortcut:a,handler:i}]of this._handlers)if(this._matchesShortcut(e,a))return e.preventDefault(),e.stopPropagation(),i(e),!0;return!1}_matchesShortcut(e,t){let a=e.key.toLowerCase(),i=t.modifiers||[];if(a!==t.key.toLowerCase())return!1;let s=i.includes("Ctrl")||i.includes("Meta"),r=i.includes("Shift"),o=i.includes("Alt"),l=(e.ctrlKey||e.metaKey)===s,p=e.shiftKey===r,A=e.altKey===o;return l&&p&&A}attach(e){this._boundHandler||(this._boundHandler=t=>this.handleEvent(t)),e.addEventListener("keydown",this._boundHandler)}detach(e){this._boundHandler&&e.removeEventListener("keydown",this._boundHandler)}};var X={light:{"--loki-bg-primary":"#fafafa","--loki-bg-secondary":"#f4f4f5","--loki-bg-tertiary":"#e4e4e7","--loki-bg-card":"#ffffff","--loki-bg-hover":"#f0f0f3","--loki-accent":"#7c3aed","--loki-accent-light":"#8b5cf6","--loki-accent-muted":"rgba(124, 58, 237, 0.12)","--loki-text-primary":"#18181b","--loki-text-secondary":"#52525b","--loki-text-muted":"#a1a1aa","--loki-border":"#e4e4e7","--loki-border-light":"#d4d4d8","--loki-green":"#16a34a","--loki-green-muted":"rgba(22, 163, 74, 0.12)","--loki-yellow":"#ca8a04","--loki-yellow-muted":"rgba(202, 138, 4, 0.12)","--loki-red":"#dc2626","--loki-red-muted":"rgba(220, 38, 38, 0.12)","--loki-blue":"#2563eb","--loki-blue-muted":"rgba(37, 99, 235, 0.12)","--loki-purple":"#9333ea","--loki-purple-muted":"rgba(147, 51, 234, 0.12)","--loki-opus":"#d97706","--loki-sonnet":"#4f46e5","--loki-haiku":"#059669","--loki-transition":"0.2s cubic-bezier(0.4, 0, 0.2, 1)"},dark:{"--loki-bg-primary":"#09090b","--loki-bg-secondary":"#0c0c0f","--loki-bg-tertiary":"#111114","--loki-bg-card":"#18181b","--loki-bg-hover":"#1f1f23","--loki-accent":"#8b5cf6","--loki-accent-light":"#a78bfa","--loki-accent-muted":"rgba(139, 92, 246, 0.15)","--loki-text-primary":"#fafafa","--loki-text-secondary":"#a1a1aa","--loki-text-muted":"#52525b","--loki-border":"rgba(255, 255, 255, 0.06)","--loki-border-light":"rgba(255, 255, 255, 0.1)","--loki-green":"#22c55e","--loki-green-muted":"rgba(34, 197, 94, 0.15)","--loki-yellow":"#eab308","--loki-yellow-muted":"rgba(234, 179, 8, 0.15)","--loki-red":"#ef4444","--loki-red-muted":"rgba(239, 68, 68, 0.15)","--loki-blue":"#3b82f6","--loki-blue-muted":"rgba(59, 130, 246, 0.15)","--loki-purple":"#a78bfa","--loki-purple-muted":"rgba(167, 139, 250, 0.15)","--loki-opus":"#f59e0b","--loki-sonnet":"#818cf8","--loki-haiku":"#34d399","--loki-transition":"0.2s cubic-bezier(0.4, 0, 0.2, 1)"}},te=`
|
|
994
994
|
:host {
|
|
995
995
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
@@ -1142,8 +1142,8 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
1142
1142
|
}
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
-
${
|
|
1146
|
-
`}getAriaPattern(e){return W[e]||{}}applyAriaPattern(e,t){let a=this.getAriaPattern(t);for(let[i,s]of Object.entries(a))if(i==="role")e.setAttribute("role",s);else{let r=i.replace(/([A-Z])/g,"-$1").toLowerCase();e.setAttribute(r,s)}}render(){}};var T={realtime:1e3,normal:2e3,background:5e3,offline:1e4},ae={vscode:T.normal,browser:T.realtime,cli:T.background},ie={baseUrl:typeof window<"u"?window.location.origin:"http://localhost:57374",wsUrl:typeof window<"u"?`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`:"ws://localhost:57374/ws",pollInterval:2e3,timeout:1e4,retryAttempts:3,retryDelay:1e3},n={CONNECTED:"api:connected",DISCONNECTED:"api:disconnected",ERROR:"api:error",STATUS_UPDATE:"api:status-update",TASK_CREATED:"api:task-created",TASK_UPDATED:"api:task-updated",TASK_DELETED:"api:task-deleted",PROJECT_CREATED:"api:project-created",PROJECT_UPDATED:"api:project-updated",AGENT_UPDATE:"api:agent-update",LOG_MESSAGE:"api:log-message",MEMORY_UPDATE:"api:memory-update"},_=class _ extends EventTarget{static getInstance(e={}){let t=e.baseUrl||ie.baseUrl;return _._instances.has(t)||_._instances.set(t,new _(e)),_._instances.get(t)}static clearInstances(){_._instances.forEach(e=>e.disconnect()),_._instances.clear()}constructor(e={}){super(),this.config={...ie,...e},this._ws=null,this._connected=!1,this._pollInterval=null,this._reconnectTimeout=null,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=ae[this._context]||T.normal,this._setupAdaptivePolling(),this._setupVSCodeBridge()}_detectContext(){return typeof acquireVsCodeApi<"u"?"vscode":typeof window<"u"&&window.location?"browser":"cli"}get context(){return this._context}static get POLL_INTERVALS(){return T}_setupAdaptivePolling(){typeof document>"u"||document.addEventListener("visibilitychange",()=>{document.hidden?this._setPollInterval(T.background):this._setPollInterval(ae[this._context]||T.normal)})}_setPollInterval(e){this._currentPollInterval=e,this._pollInterval&&(this.stopPolling(),this.startPolling(null,e))}setPollMode(e){let t=T[e];t&&this._setPollInterval(t)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}window.addEventListener("message",e=>{let t=e.data;if(!(!t||!t.type))switch(t.type){case"updateStatus":this._emit(n.STATUS_UPDATE,t.data);break;case"updateTasks":this._emit(n.TASK_UPDATED,t.data);break;case"taskCreated":this._emit(n.TASK_CREATED,t.data);break;case"taskDeleted":this._emit(n.TASK_DELETED,t.data);break;case"projectCreated":this._emit(n.PROJECT_CREATED,t.data);break;case"projectUpdated":this._emit(n.PROJECT_UPDATED,t.data);break;case"agentUpdate":this._emit(n.AGENT_UPDATE,t.data);break;case"logMessage":this._emit(n.LOG_MESSAGE,t.data);break;case"memoryUpdate":this._emit(n.MEMORY_UPDATE,t.data);break;case"connected":this._connected=!0,this._emit(n.CONNECTED,t.data);break;case"disconnected":this._connected=!1,this._emit(n.DISCONNECTED,t.data);break;case"error":this._emit(n.ERROR,t.data);break;case"setPollMode":this.setPollMode(t.data.mode);break;default:this._emit(`api:${t.type}`,t.data)}})}}get isVSCode(){return this._context==="vscode"}postToVSCode(e,t={}){this._vscodeApi&&this._vscodeApi.postMessage({type:e,data:t})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(e,t={}){this.postToVSCode("userAction",{action:e,...t})}get baseUrl(){return this.config.baseUrl}set baseUrl(e){this.config.baseUrl=e,this.config.wsUrl=e.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((e,t)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._emit(n.CONNECTED),e()},this._ws.onclose=()=>{this._connected=!1,this._emit(n.DISCONNECTED),this._scheduleReconnect()},this._ws.onerror=a=>{this._emit(n.ERROR,{error:a}),t(a)},this._ws.onmessage=a=>{try{let i=JSON.parse(a.data);this._handleMessage(i)}catch(i){console.error("Failed to parse WebSocket message:",i)}}}catch(a){t(a)}})}disconnect(){this._ws&&(this._ws.close(),this._ws=null),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null),this._connected=!1}_scheduleReconnect(){this._reconnectTimeout||(this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},this.config.retryDelay))}_handleMessage(e){let a={connected:n.CONNECTED,status_update:n.STATUS_UPDATE,task_created:n.TASK_CREATED,task_updated:n.TASK_UPDATED,task_deleted:n.TASK_DELETED,task_moved:n.TASK_UPDATED,project_created:n.PROJECT_CREATED,project_updated:n.PROJECT_UPDATED,agent_update:n.AGENT_UPDATE,log:n.LOG_MESSAGE}[e.type]||`api:${e.type}`;this._emit(a,e.data)}_emit(e,t={}){this.dispatchEvent(new CustomEvent(e,{detail:t}))}async _request(e,t={}){let a=`${this.config.baseUrl}${e}`,i=new AbortController,s=setTimeout(()=>i.abort(),this.config.timeout);try{let r=await fetch(a,{...t,signal:i.signal,headers:{"Content-Type":"application/json",...t.headers}});if(clearTimeout(s),!r.ok){let o=await r.json().catch(()=>({detail:r.statusText}));throw new Error(o.detail||`HTTP ${r.status}`)}return r.status===204?null:await r.json()}catch(r){throw clearTimeout(s),r.name==="AbortError"?new Error("Request timeout"):r}}async _get(e,t=!1){if(t&&this._cache.has(e)){let i=this._cache.get(e);if(Date.now()-i.timestamp<this._cacheTimeout)return i.data}let a=await this._request(e);return t&&this._cache.set(e,{data:a,timestamp:Date.now()}),a}async _post(e,t){return this._request(e,{method:"POST",body:JSON.stringify(t)})}async _put(e,t){return this._request(e,{method:"PUT",body:JSON.stringify(t)})}async _delete(e){return this._request(e,{method:"DELETE"})}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(e=null){let t=e?`?status=${e}`:"";return this._get(`/api/projects${t}`)}async getProject(e){return this._get(`/api/projects/${e}`)}async createProject(e){return this._post("/api/projects",e)}async updateProject(e,t){return this._put(`/api/projects/${e}`,t)}async deleteProject(e){return this._delete(`/api/projects/${e}`)}async listTasks(e={}){let t=new URLSearchParams;e.projectId&&t.append("project_id",e.projectId),e.status&&t.append("status",e.status),e.priority&&t.append("priority",e.priority);let a=t.toString()?`?${t}`:"";return this._get(`/api/tasks${a}`)}async getTask(e){return this._get(`/api/tasks/${e}`)}async createTask(e){return this._post("/api/tasks",e)}async updateTask(e,t){return this._put(`/api/tasks/${e}`,t)}async moveTask(e,t,a){return this._post(`/api/tasks/${e}/move`,{status:t,position:a})}async deleteTask(e){return this._delete(`/api/tasks/${e}`)}async getMemorySummary(){return this._get("/api/memory/summary",!0)}async getMemoryIndex(){return this._get("/api/memory/index",!0)}async getMemoryTimeline(){return this._get("/api/memory/timeline")}async listEpisodes(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/episodes${t?"?"+t:""}`)}async getEpisode(e){return this._get(`/api/memory/episodes/${e}`)}async listPatterns(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/patterns${t?"?"+t:""}`)}async getPattern(e){return this._get(`/api/memory/patterns/${e}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(e){return this._get(`/api/memory/skills/${e}`)}async retrieveMemories(e,t=null,a=5){return this._post("/api/memory/retrieve",{query:e,taskType:t,topK:a})}async consolidateMemory(e=24){return this._post("/api/memory/consolidate",{sinceHours:e})}async getTokenEconomics(){return this._get("/api/memory/economics")}async listRegisteredProjects(e=!1){return this._get(`/api/registry/projects?include_inactive=${e}`)}async registerProject(e,t=null,a=null){return this._post("/api/registry/projects",{path:e,name:t,alias:a})}async discoverProjects(e=3){return this._get(`/api/registry/discover?max_depth=${e}`)}async syncRegistry(){return this._post("/api/registry/sync",{})}async getCrossProjectTasks(e=null){let t=e?`?project_ids=${e.join(",")}`:"";return this._get(`/api/registry/tasks${t}`)}async getLearningMetrics(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/metrics${a}`)}async getLearningTrends(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/trends${a}`)}async getLearningSignals(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source),e.limit&&t.append("limit",String(e.limit)),e.offset&&t.append("offset",String(e.offset));let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/signals${a}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(e={}){return this._post("/api/learning/aggregate",e)}async getAggregatedPreferences(e=20){return this._get(`/api/learning/preferences?limit=${e}`)}async getAggregatedErrors(e=20){return this._get(`/api/learning/errors?limit=${e}`)}async getAggregatedSuccessPatterns(e=20){return this._get(`/api/learning/success?limit=${e}`)}async getToolEfficiency(e=20){return this._get(`/api/learning/tools?limit=${e}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getContext(){return this._get("/api/context")}async getNotifications(e,t){let a=new URLSearchParams;e&&a.set("severity",e),t&&a.set("unread_only","true");let i=a.toString();return this._get("/api/notifications"+(i?"?"+i:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(e){return this._put("/api/notifications/triggers",{triggers:e})}async acknowledgeNotification(e){return this._post("/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{})}async pauseSession(){return this._post("/api/control/pause",{})}async resumeSession(){return this._post("/api/control/resume",{})}async stopSession(){return this._post("/api/control/stop",{})}async getLogs(e=100){return this._get(`/api/logs?lines=${e}`)}startPolling(e,t=null){if(this._pollInterval)return;this._pollCallback=e;let a=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(n.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(n.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};a();let i=t||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(a,i)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};k(_,"_instances",new Map);var I=_;function se(d={}){return new I(d)}function u(d={}){return I.getInstance(d)}var Q="loki-state-change",Z={ui:{theme:"light",sidebarCollapsed:!1,activeSection:"kanban",terminalAutoScroll:!0},session:{connected:!1,lastSync:null,mode:"offline",phase:null,iteration:null},localTasks:[],cache:{projects:[],tasks:[],agents:[],memory:null,lastFetch:null},preferences:{pollInterval:2e3,notifications:!0,soundEnabled:!1}},f=class f extends EventTarget{static getInstance(){return f._instance||(f._instance=new f),f._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let e=localStorage.getItem(f.STORAGE_KEY);if(e){let t=JSON.parse(e);return this._mergeState(Z,t)}}catch(e){console.warn("Failed to load state from localStorage:",e)}return{...Z}}_mergeState(e,t){let a={...e};for(let i of Object.keys(t))i in e&&typeof e[i]=="object"&&!Array.isArray(e[i])?a[i]=this._mergeState(e[i],t[i]):a[i]=t[i];return a}_saveState(){try{let e={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem(f.STORAGE_KEY,JSON.stringify(e))}catch(e){console.warn("Failed to save state to localStorage:",e)}}get(e=null){if(!e)return{...this._state};let t=e.split("."),a=this._state;for(let i of t){if(a==null)return;a=a[i]}return a}set(e,t,a=!0){let i=e.split("."),s=i.pop(),r=this._state;for(let l of i)l in r||(r[l]={}),r=r[l];let o=r[s];r[s]=t,a&&this._saveState(),this._notifyChange(e,t,o)}update(e,t=!0){let a=[];for(let[i,s]of Object.entries(e)){let r=this.get(i);this.set(i,s,!1),a.push({path:i,value:s,oldValue:r})}t&&this._saveState();for(let i of a)this._notifyChange(i.path,i.value,i.oldValue)}_notifyChange(e,t,a){this.dispatchEvent(new CustomEvent(Q,{detail:{path:e,value:t,oldValue:a}}));let i=this._subscribers.get(e)||[];for(let r of i)try{r(t,a,e)}catch(o){console.error("State subscriber error:",o)}let s=e.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let l of o)try{l(this.get(r),null,r)}catch(p){console.error("State subscriber error:",p)}}}subscribe(e,t){return this._subscribers.has(e)||this._subscribers.set(e,[]),this._subscribers.get(e).push(t),()=>{let a=this._subscribers.get(e),i=a.indexOf(t);i>-1&&a.splice(i,1)}}reset(e=null){if(e){let t=e.split("."),a=Z;for(let i of t)a=a?.[i];this.set(e,a)}else this._state={...Z},this._saveState(),this.dispatchEvent(new CustomEvent(Q,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(e){let t=this.get("localTasks")||[],a={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...e};return this.set("localTasks",[...t,a]),a}updateLocalTask(e,t){let a=this.get("localTasks")||[],i=a.findIndex(r=>r.id===e);if(i===-1)return null;let s={...a[i],...t,updatedAt:new Date().toISOString()};return a[i]=s,this.set("localTasks",[...a]),s}deleteLocalTask(e){let t=this.get("localTasks")||[];this.set("localTasks",t.filter(a=>a.id!==e))}moveLocalTask(e,t,a=null){let s=(this.get("localTasks")||[]).find(r=>r.id===e);return s?this.updateLocalTask(e,{status:t,position:a??s.position}):null}updateSession(e){this.update(Object.fromEntries(Object.entries(e).map(([t,a])=>[`session.${t}`,a])),!1)}updateCache(e){this.update({"cache.projects":e.projects??this.get("cache.projects"),"cache.tasks":e.tasks??this.get("cache.tasks"),"cache.agents":e.agents??this.get("cache.agents"),"cache.memory":e.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let e=this.get("cache.tasks")||[],a=(this.get("localTasks")||[]).map(i=>({...i,isLocal:!0}));return[...e,...a]}getTasksByStatus(e){return this.getMergedTasks().filter(t=>t.status===e)}};k(f,"STORAGE_KEY","loki-dashboard-state"),k(f,"_instance",null);var M=f;function C(){return M.getInstance()}function re(d){let e=C();return{get:()=>e.get(d),set:t=>e.set(d,t),subscribe:t=>e.subscribe(d,t)}}var z=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={status:"offline",phase:null,iteration:null,provider:null,running_agents:0,pending_tasks:null,uptime_seconds:0,complexity:null,connected:!1},this._api=null,this._pollInterval=null,this._statusUpdateHandler=null,this._connectedHandler=null,this._disconnectedHandler=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(n.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(n.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadStatus()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:e}),this._statusUpdateHandler=t=>this._updateFromStatus(t.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(n.CONNECTED,this._connectedHandler),this._api.addEventListener(n.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(e){e&&(this._data={...this._data,connected:!0,status:e.status||"offline",phase:e.phase||null,iteration:e.iteration!=null?e.iteration:null,provider:e.provider||null,running_agents:e.running_agents||0,pending_tasks:e.pending_tasks!=null?e.pending_tasks:null,uptime_seconds:e.uptime_seconds||0,complexity:e.complexity||null},this.render())}_startPolling(){this._pollInterval=setInterval(async()=>{try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._data.connected=!1,this._data.status="offline",this.render()}},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_formatUptime(e){if(!e||e<0)return"--";let t=Math.floor(e/3600),a=Math.floor(e%3600/60),i=Math.floor(e%60);return t>0?`${t}h ${a}m`:a>0?`${a}m ${i}s`:`${i}s`}_getStatusDotClass(){switch(this._data.status){case"running":case"autonomous":return"active";case"paused":return"paused";case"stopped":return"stopped";case"error":return"error";default:return"offline"}}render(){let e=this._getStatusDotClass(),t=(this._data.status||"OFFLINE").toUpperCase(),a=this._data.phase||"--",i=this._data.iteration!=null?String(this._data.iteration):"0",s=(this._data.provider||"CLAUDE").toUpperCase(),r=String(this._data.running_agents||0),o=this._data.pending_tasks!=null?`${this._data.pending_tasks} pending`:"--",l=this._formatUptime(this._data.uptime_seconds),p=(this._data.complexity||"STANDARD").toUpperCase();this.shadowRoot.innerHTML=`
|
|
1145
|
+
${M}
|
|
1146
|
+
`}getAriaPattern(e){return W[e]||{}}applyAriaPattern(e,t){let a=this.getAriaPattern(t);for(let[i,s]of Object.entries(a))if(i==="role")e.setAttribute("role",s);else{let r=i.replace(/([A-Z])/g,"-$1").toLowerCase();e.setAttribute(r,s)}}render(){}};var T={realtime:1e3,normal:2e3,background:5e3,offline:1e4},ae={vscode:T.normal,browser:T.realtime,cli:T.background},ie={baseUrl:typeof window<"u"?window.location.origin:"http://localhost:57374",wsUrl:typeof window<"u"?`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`:"ws://localhost:57374/ws",pollInterval:2e3,timeout:1e4,retryAttempts:3,retryDelay:1e3},n={CONNECTED:"api:connected",DISCONNECTED:"api:disconnected",ERROR:"api:error",STATUS_UPDATE:"api:status-update",TASK_CREATED:"api:task-created",TASK_UPDATED:"api:task-updated",TASK_DELETED:"api:task-deleted",PROJECT_CREATED:"api:project-created",PROJECT_UPDATED:"api:project-updated",AGENT_UPDATE:"api:agent-update",LOG_MESSAGE:"api:log-message",MEMORY_UPDATE:"api:memory-update"},_=class _ extends EventTarget{static getInstance(e={}){let t=e.baseUrl||ie.baseUrl;return _._instances.has(t)||_._instances.set(t,new _(e)),_._instances.get(t)}static clearInstances(){_._instances.forEach(e=>e.disconnect()),_._instances.clear()}constructor(e={}){super(),this.config={...ie,...e},this._ws=null,this._connected=!1,this._pollInterval=null,this._reconnectTimeout=null,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=ae[this._context]||T.normal,this._visibilityChangeHandler=null,this._messageHandler=null,this._setupAdaptivePolling(),this._setupVSCodeBridge()}_detectContext(){return typeof acquireVsCodeApi<"u"?"vscode":typeof window<"u"&&window.location?"browser":"cli"}get context(){return this._context}static get POLL_INTERVALS(){return T}_setupAdaptivePolling(){typeof document>"u"||(this._visibilityChangeHandler=()=>{document.hidden?this._setPollInterval(T.background):this._setPollInterval(ae[this._context]||T.normal)},document.addEventListener("visibilitychange",this._visibilityChangeHandler))}_setPollInterval(e){this._currentPollInterval=e,this._pollInterval&&(this.stopPolling(),this.startPolling(null,e))}setPollMode(e){let t=T[e];t&&this._setPollInterval(t)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}this._messageHandler=e=>{let t=e.data;if(!(!t||!t.type))switch(t.type){case"updateStatus":this._emit(n.STATUS_UPDATE,t.data);break;case"updateTasks":this._emit(n.TASK_UPDATED,t.data);break;case"taskCreated":this._emit(n.TASK_CREATED,t.data);break;case"taskDeleted":this._emit(n.TASK_DELETED,t.data);break;case"projectCreated":this._emit(n.PROJECT_CREATED,t.data);break;case"projectUpdated":this._emit(n.PROJECT_UPDATED,t.data);break;case"agentUpdate":this._emit(n.AGENT_UPDATE,t.data);break;case"logMessage":this._emit(n.LOG_MESSAGE,t.data);break;case"memoryUpdate":this._emit(n.MEMORY_UPDATE,t.data);break;case"connected":this._connected=!0,this._emit(n.CONNECTED,t.data);break;case"disconnected":this._connected=!1,this._emit(n.DISCONNECTED,t.data);break;case"error":this._emit(n.ERROR,t.data);break;case"setPollMode":this.setPollMode(t.data.mode);break;default:this._emit(`api:${t.type}`,t.data)}},window.addEventListener("message",this._messageHandler)}}get isVSCode(){return this._context==="vscode"}postToVSCode(e,t={}){this._vscodeApi&&this._vscodeApi.postMessage({type:e,data:t})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(e,t={}){this.postToVSCode("userAction",{action:e,...t})}get baseUrl(){return this.config.baseUrl}set baseUrl(e){this.config.baseUrl=e,this.config.wsUrl=e.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((e,t)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._emit(n.CONNECTED),e()},this._ws.onclose=()=>{this._connected=!1,this._emit(n.DISCONNECTED),this._scheduleReconnect()},this._ws.onerror=a=>{this._emit(n.ERROR,{error:a}),t(a)},this._ws.onmessage=a=>{try{let i=JSON.parse(a.data);this._handleMessage(i)}catch(i){console.error("Failed to parse WebSocket message:",i)}}}catch(a){t(a)}})}disconnect(){this._ws&&(this._ws.close(),this._ws=null),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null),this._connected=!1,this._cleanupGlobalListeners()}_cleanupGlobalListeners(){this._visibilityChangeHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this._visibilityChangeHandler),this._visibilityChangeHandler=null),this._messageHandler&&typeof window<"u"&&(window.removeEventListener("message",this._messageHandler),this._messageHandler=null)}destroy(){this.disconnect()}_scheduleReconnect(){this._reconnectTimeout||(this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},this.config.retryDelay))}_handleMessage(e){let a={connected:n.CONNECTED,status_update:n.STATUS_UPDATE,task_created:n.TASK_CREATED,task_updated:n.TASK_UPDATED,task_deleted:n.TASK_DELETED,task_moved:n.TASK_UPDATED,project_created:n.PROJECT_CREATED,project_updated:n.PROJECT_UPDATED,agent_update:n.AGENT_UPDATE,log:n.LOG_MESSAGE}[e.type]||`api:${e.type}`;this._emit(a,e.data)}_emit(e,t={}){this.dispatchEvent(new CustomEvent(e,{detail:t}))}async _request(e,t={}){let a=`${this.config.baseUrl}${e}`,i=new AbortController,s=setTimeout(()=>i.abort(),this.config.timeout);try{let r=await fetch(a,{...t,signal:i.signal,headers:{"Content-Type":"application/json",...t.headers}});if(clearTimeout(s),!r.ok){let o=await r.json().catch(()=>({detail:r.statusText}));throw new Error(o.detail||`HTTP ${r.status}`)}return r.status===204?null:await r.json()}catch(r){throw clearTimeout(s),r.name==="AbortError"?new Error("Request timeout"):r}}async _get(e,t=!1){if(t&&this._cache.has(e)){let i=this._cache.get(e);if(Date.now()-i.timestamp<this._cacheTimeout)return i.data}let a=await this._request(e);return t&&this._cache.set(e,{data:a,timestamp:Date.now()}),a}async _post(e,t){return this._request(e,{method:"POST",body:JSON.stringify(t)})}async _put(e,t){return this._request(e,{method:"PUT",body:JSON.stringify(t)})}async _delete(e){return this._request(e,{method:"DELETE"})}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(e=null){let t=e?`?status=${e}`:"";return this._get(`/api/projects${t}`)}async getProject(e){return this._get(`/api/projects/${e}`)}async createProject(e){return this._post("/api/projects",e)}async updateProject(e,t){return this._put(`/api/projects/${e}`,t)}async deleteProject(e){return this._delete(`/api/projects/${e}`)}async listTasks(e={}){let t=new URLSearchParams;e.projectId&&t.append("project_id",e.projectId),e.status&&t.append("status",e.status),e.priority&&t.append("priority",e.priority);let a=t.toString()?`?${t}`:"";return this._get(`/api/tasks${a}`)}async getTask(e){return this._get(`/api/tasks/${e}`)}async createTask(e){return this._post("/api/tasks",e)}async updateTask(e,t){return this._put(`/api/tasks/${e}`,t)}async moveTask(e,t,a){return this._post(`/api/tasks/${e}/move`,{status:t,position:a})}async deleteTask(e){return this._delete(`/api/tasks/${e}`)}async getMemorySummary(){return this._get("/api/memory/summary",!0)}async getMemoryIndex(){return this._get("/api/memory/index",!0)}async getMemoryTimeline(){return this._get("/api/memory/timeline")}async listEpisodes(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/episodes${t?"?"+t:""}`)}async getEpisode(e){return this._get(`/api/memory/episodes/${e}`)}async listPatterns(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/patterns${t?"?"+t:""}`)}async getPattern(e){return this._get(`/api/memory/patterns/${e}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(e){return this._get(`/api/memory/skills/${e}`)}async retrieveMemories(e,t=null,a=5){return this._post("/api/memory/retrieve",{query:e,taskType:t,topK:a})}async consolidateMemory(e=24){return this._post("/api/memory/consolidate",{sinceHours:e})}async getTokenEconomics(){return this._get("/api/memory/economics")}async listRegisteredProjects(e=!1){return this._get(`/api/registry/projects?include_inactive=${e}`)}async registerProject(e,t=null,a=null){return this._post("/api/registry/projects",{path:e,name:t,alias:a})}async discoverProjects(e=3){return this._get(`/api/registry/discover?max_depth=${e}`)}async syncRegistry(){return this._post("/api/registry/sync",{})}async getCrossProjectTasks(e=null){let t=e?`?project_ids=${e.join(",")}`:"";return this._get(`/api/registry/tasks${t}`)}async getLearningMetrics(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/metrics${a}`)}async getLearningTrends(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/trends${a}`)}async getLearningSignals(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source),e.limit&&t.append("limit",String(e.limit)),e.offset&&t.append("offset",String(e.offset));let a=t.toString()?`?${t}`:"";return this._get(`/api/learning/signals${a}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(e={}){return this._post("/api/learning/aggregate",e)}async getAggregatedPreferences(e=20){return this._get(`/api/learning/preferences?limit=${e}`)}async getAggregatedErrors(e=20){return this._get(`/api/learning/errors?limit=${e}`)}async getAggregatedSuccessPatterns(e=20){return this._get(`/api/learning/success?limit=${e}`)}async getToolEfficiency(e=20){return this._get(`/api/learning/tools?limit=${e}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getContext(){return this._get("/api/context")}async getNotifications(e,t){let a=new URLSearchParams;e&&a.set("severity",e),t&&a.set("unread_only","true");let i=a.toString();return this._get("/api/notifications"+(i?"?"+i:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(e){return this._put("/api/notifications/triggers",{triggers:e})}async acknowledgeNotification(e){return this._post("/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{})}async pauseSession(){return this._post("/api/control/pause",{})}async resumeSession(){return this._post("/api/control/resume",{})}async stopSession(){return this._post("/api/control/stop",{})}async getLogs(e=100){return this._get(`/api/logs?lines=${e}`)}startPolling(e,t=null){if(this._pollInterval)return;this._pollCallback=e;let a=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(n.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(n.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};a();let i=t||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(a,i)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};k(_,"_instances",new Map);var I=_;function se(d={}){return new I(d)}function u(d={}){return I.getInstance(d)}var Q="loki-state-change",Z={ui:{theme:"light",sidebarCollapsed:!1,activeSection:"kanban",terminalAutoScroll:!0},session:{connected:!1,lastSync:null,mode:"offline",phase:null,iteration:null},localTasks:[],cache:{projects:[],tasks:[],agents:[],memory:null,lastFetch:null},preferences:{pollInterval:2e3,notifications:!0,soundEnabled:!1}},f=class f extends EventTarget{static getInstance(){return f._instance||(f._instance=new f),f._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let e=localStorage.getItem(f.STORAGE_KEY);if(e){let t=JSON.parse(e);return this._mergeState(Z,t)}}catch(e){console.warn("Failed to load state from localStorage:",e)}return{...Z}}_mergeState(e,t){let a={...e};for(let i of Object.keys(t))i in e&&typeof e[i]=="object"&&!Array.isArray(e[i])?a[i]=this._mergeState(e[i],t[i]):a[i]=t[i];return a}_saveState(){try{let e={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem(f.STORAGE_KEY,JSON.stringify(e))}catch(e){console.warn("Failed to save state to localStorage:",e)}}get(e=null){if(!e)return{...this._state};let t=e.split("."),a=this._state;for(let i of t){if(a==null)return;a=a[i]}return a}set(e,t,a=!0){let i=e.split("."),s=i.pop(),r=this._state;for(let l of i)l in r||(r[l]={}),r=r[l];let o=r[s];r[s]=t,a&&this._saveState(),this._notifyChange(e,t,o)}update(e,t=!0){let a=[];for(let[i,s]of Object.entries(e)){let r=this.get(i);this.set(i,s,!1),a.push({path:i,value:s,oldValue:r})}t&&this._saveState();for(let i of a)this._notifyChange(i.path,i.value,i.oldValue)}_notifyChange(e,t,a){this.dispatchEvent(new CustomEvent(Q,{detail:{path:e,value:t,oldValue:a}}));let i=this._subscribers.get(e)||[];for(let r of i)try{r(t,a,e)}catch(o){console.error("State subscriber error:",o)}let s=e.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let l of o)try{l(this.get(r),null,r)}catch(p){console.error("State subscriber error:",p)}}}subscribe(e,t){return this._subscribers.has(e)||this._subscribers.set(e,[]),this._subscribers.get(e).push(t),()=>{let a=this._subscribers.get(e),i=a.indexOf(t);i>-1&&a.splice(i,1)}}reset(e=null){if(e){let t=e.split("."),a=Z;for(let i of t)a=a?.[i];this.set(e,a)}else this._state={...Z},this._saveState(),this.dispatchEvent(new CustomEvent(Q,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(e){let t=this.get("localTasks")||[],a={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...e};return this.set("localTasks",[...t,a]),a}updateLocalTask(e,t){let a=this.get("localTasks")||[],i=a.findIndex(r=>r.id===e);if(i===-1)return null;let s={...a[i],...t,updatedAt:new Date().toISOString()};return a[i]=s,this.set("localTasks",[...a]),s}deleteLocalTask(e){let t=this.get("localTasks")||[];this.set("localTasks",t.filter(a=>a.id!==e))}moveLocalTask(e,t,a=null){let s=(this.get("localTasks")||[]).find(r=>r.id===e);return s?this.updateLocalTask(e,{status:t,position:a??s.position}):null}updateSession(e){this.update(Object.fromEntries(Object.entries(e).map(([t,a])=>[`session.${t}`,a])),!1)}updateCache(e){this.update({"cache.projects":e.projects??this.get("cache.projects"),"cache.tasks":e.tasks??this.get("cache.tasks"),"cache.agents":e.agents??this.get("cache.agents"),"cache.memory":e.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let e=this.get("cache.tasks")||[],a=(this.get("localTasks")||[]).map(i=>({...i,isLocal:!0}));return[...e,...a]}getTasksByStatus(e){return this.getMergedTasks().filter(t=>t.status===e)}};k(f,"STORAGE_KEY","loki-dashboard-state"),k(f,"_instance",null);var R=f;function C(){return R.getInstance()}function re(d){let e=C();return{get:()=>e.get(d),set:t=>e.set(d,t),subscribe:t=>e.subscribe(d,t)}}var z=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={status:"offline",phase:null,iteration:null,provider:null,running_agents:0,pending_tasks:null,uptime_seconds:0,complexity:null,connected:!1},this._api=null,this._pollInterval=null,this._statusUpdateHandler=null,this._connectedHandler=null,this._disconnectedHandler=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(n.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(n.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadStatus()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:e}),this._statusUpdateHandler=t=>this._updateFromStatus(t.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(n.CONNECTED,this._connectedHandler),this._api.addEventListener(n.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(e){e&&(this._data={...this._data,connected:!0,status:e.status||"offline",phase:e.phase||null,iteration:e.iteration!=null?e.iteration:null,provider:e.provider||null,running_agents:e.running_agents||0,pending_tasks:e.pending_tasks!=null?e.pending_tasks:null,uptime_seconds:e.uptime_seconds||0,complexity:e.complexity||null},this.render())}_startPolling(){this._pollInterval=setInterval(async()=>{try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._data.connected=!1,this._data.status="offline",this.render()}},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_formatUptime(e){if(!e||e<0)return"--";let t=Math.floor(e/3600),a=Math.floor(e%3600/60),i=Math.floor(e%60);return t>0?`${t}h ${a}m`:a>0?`${a}m ${i}s`:`${i}s`}_getStatusDotClass(){switch(this._data.status){case"running":case"autonomous":return"active";case"paused":return"paused";case"stopped":return"stopped";case"error":return"error";default:return"offline"}}render(){let e=this._getStatusDotClass(),t=(this._data.status||"OFFLINE").toUpperCase(),a=this._data.phase||"--",i=this._data.iteration!=null?String(this._data.iteration):"0",s=(this._data.provider||"CLAUDE").toUpperCase(),r=String(this._data.running_agents||0),o=this._data.pending_tasks!=null?`${this._data.pending_tasks} pending`:"--",l=this._formatUptime(this._data.uptime_seconds),p=(this._data.complexity||"STANDARD").toUpperCase();this.shadowRoot.innerHTML=`
|
|
1147
1147
|
<style>
|
|
1148
1148
|
${this.getBaseStyles()}
|
|
1149
1149
|
|
|
@@ -1310,7 +1310,7 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
1310
1310
|
</div>
|
|
1311
1311
|
</div>
|
|
1312
1312
|
</div>
|
|
1313
|
-
`}};customElements.get("loki-overview")||customElements.define("loki-overview",z);var ke=[{id:"pending",label:"Pending",status:"pending",color:"var(--loki-text-muted)"},{id:"in_progress",label:"In Progress",status:"in_progress",color:"var(--loki-blue)"},{id:"review",label:"In Review",status:"review",color:"var(--loki-purple)"},{id:"done",label:"Completed",status:"done",color:"var(--loki-green)"}];var
|
|
1313
|
+
`}};customElements.get("loki-overview")||customElements.define("loki-overview",z);var ke=[{id:"pending",label:"Pending",status:"pending",color:"var(--loki-text-muted)"},{id:"in_progress",label:"In Progress",status:"in_progress",color:"var(--loki-blue)"},{id:"review",label:"In Review",status:"review",color:"var(--loki-purple)"},{id:"done",label:"Completed",status:"done",color:"var(--loki-green)"}];var H=class extends c{static get observedAttributes(){return["api-url","project-id","theme","readonly"]}constructor(){super(),this._tasks=[],this._loading=!0,this._error=null,this._draggedTask=null,this._api=null,this._state=C()}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadTasks()}disconnectedCallback(){super.disconnectedCallback(),this._api&&(this._api.removeEventListener(n.TASK_CREATED,this._onTaskEvent),this._api.removeEventListener(n.TASK_UPDATED,this._onTaskEvent),this._api.removeEventListener(n.TASK_DELETED,this._onTaskEvent))}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadTasks()),e==="project-id"&&this._loadTasks(),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:e}),this._onTaskEvent=()=>this._loadTasks(),this._api.addEventListener(n.TASK_CREATED,this._onTaskEvent),this._api.addEventListener(n.TASK_UPDATED,this._onTaskEvent),this._api.addEventListener(n.TASK_DELETED,this._onTaskEvent)}async _loadTasks(){this._loading=!0,this._error=null,this.render();try{let e=this.getAttribute("project-id"),t=e?{projectId:parseInt(e)}:{};this._tasks=await this._api.listTasks(t);let a=this._state.get("localTasks")||[];a.length>0&&(this._tasks=[...this._tasks,...a.map(i=>({...i,isLocal:!0}))]),this._state.update({"cache.tasks":this._tasks},!1)}catch(e){this._error=e.message,this._tasks=(this._state.get("localTasks")||[]).map(t=>({...t,isLocal:!0}))}this._loading=!1,this.render()}_getTasksByStatus(e){return this._tasks.filter(t=>t.status?.toLowerCase().replace(/-/g,"_")===e)}_handleDragStart(e,t){this.hasAttribute("readonly")||(this._draggedTask=t,e.target.classList.add("dragging"),e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",t.id.toString()))}_handleDragEnd(e){e.target.classList.remove("dragging"),this._draggedTask=null,this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.classList.remove("drag-over")})}_handleDragOver(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}_handleDragEnter(e){e.preventDefault(),e.currentTarget.classList.add("drag-over")}_handleDragLeave(e){e.currentTarget.contains(e.relatedTarget)||e.currentTarget.classList.remove("drag-over")}async _handleDrop(e,t){if(e.preventDefault(),e.currentTarget.classList.remove("drag-over"),!this._draggedTask||this.hasAttribute("readonly"))return;let a=this._draggedTask.id,i=this._tasks.find(r=>r.id===a);if(!i)return;let s=i.status;if(s!==t){i.status=t,this.render();try{i.isLocal?this._state.moveLocalTask(a,t):await this._api.moveTask(a,t,0),this.dispatchEvent(new CustomEvent("task-moved",{detail:{taskId:a,oldStatus:s,newStatus:t}}))}catch(r){i.status=s,this.render(),console.error("Failed to move task:",r)}}}_openAddTaskModal(e="pending"){this.dispatchEvent(new CustomEvent("add-task",{detail:{status:e}}))}_openTaskDetail(e){this.dispatchEvent(new CustomEvent("task-click",{detail:{task:e}}))}render(){let e=`
|
|
1314
1314
|
<style>
|
|
1315
1315
|
${this.getBaseStyles()}
|
|
1316
1316
|
|
|
@@ -1605,7 +1605,7 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
1605
1605
|
</div>
|
|
1606
1606
|
${a}
|
|
1607
1607
|
</div>
|
|
1608
|
-
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("refresh-btn");e&&e.addEventListener("click",()=>this._loadTasks()),this.shadowRoot.querySelectorAll(".add-task-btn").forEach(t=>{t.addEventListener("click",()=>{this._openAddTaskModal(t.dataset.status)})}),this.shadowRoot.querySelectorAll(".task-card").forEach(t=>{let a=t.dataset.taskId,i=this._tasks.find(s=>s.id.toString()===a);i&&(t.addEventListener("click",()=>this._openTaskDetail(i)),t.addEventListener("keydown",s=>{s.key==="Enter"||s.key===" "?(s.preventDefault(),this._openTaskDetail(i)):(s.key==="ArrowDown"||s.key==="ArrowUp")&&(s.preventDefault(),this._navigateTaskCards(t,s.key==="ArrowDown"?"next":"prev"))}),t.classList.contains("draggable")&&(t.addEventListener("dragstart",s=>this._handleDragStart(s,i)),t.addEventListener("dragend",s=>this._handleDragEnd(s))))}),this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.addEventListener("dragover",a=>this._handleDragOver(a)),t.addEventListener("dragenter",a=>this._handleDragEnter(a)),t.addEventListener("dragleave",a=>this._handleDragLeave(a)),t.addEventListener("drop",a=>this._handleDrop(a,t.dataset.status))})}_escapeHtml(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}_navigateTaskCards(e,t){let a=Array.from(this.shadowRoot.querySelectorAll(".task-card")),i=a.indexOf(e);if(i===-1)return;let s=t==="next"?i+1:i-1;s>=0&&s<a.length&&a[s].focus()}};customElements.get("loki-task-board")||customElements.define("loki-task-board",
|
|
1608
|
+
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("refresh-btn");e&&e.addEventListener("click",()=>this._loadTasks()),this.shadowRoot.querySelectorAll(".add-task-btn").forEach(t=>{t.addEventListener("click",()=>{this._openAddTaskModal(t.dataset.status)})}),this.shadowRoot.querySelectorAll(".task-card").forEach(t=>{let a=t.dataset.taskId,i=this._tasks.find(s=>s.id.toString()===a);i&&(t.addEventListener("click",()=>this._openTaskDetail(i)),t.addEventListener("keydown",s=>{s.key==="Enter"||s.key===" "?(s.preventDefault(),this._openTaskDetail(i)):(s.key==="ArrowDown"||s.key==="ArrowUp")&&(s.preventDefault(),this._navigateTaskCards(t,s.key==="ArrowDown"?"next":"prev"))}),t.classList.contains("draggable")&&(t.addEventListener("dragstart",s=>this._handleDragStart(s,i)),t.addEventListener("dragend",s=>this._handleDragEnd(s))))}),this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.addEventListener("dragover",a=>this._handleDragOver(a)),t.addEventListener("dragenter",a=>this._handleDragEnter(a)),t.addEventListener("dragleave",a=>this._handleDragLeave(a)),t.addEventListener("drop",a=>this._handleDrop(a,t.dataset.status))})}_escapeHtml(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}_navigateTaskCards(e,t){let a=Array.from(this.shadowRoot.querySelectorAll(".task-card")),i=a.indexOf(e);if(i===-1)return;let s=t==="next"?i+1:i-1;s>=0&&s<a.length&&a[s].focus()}};customElements.get("loki-task-board")||customElements.define("loki-task-board",H);var B=class extends c{static get observedAttributes(){return["api-url","theme","compact"]}constructor(){super(),this._status={mode:"offline",phase:null,iteration:null,complexity:null,connected:!1,version:null,uptime:0,activeAgents:0,pendingTasks:0},this._api=null,this._state=C(),this._statusUpdateHandler=null,this._connectedHandler=null,this._disconnectedHandler=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(n.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(n.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadStatus()),e==="theme"&&this._applyTheme(),e==="compact"&&this.render())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:e}),this._statusUpdateHandler=t=>this._updateFromStatus(t.detail),this._connectedHandler=()=>{this._status.connected=!0,this.render()},this._disconnectedHandler=()=>{this._status.connected=!1,this._status.mode="offline",this.render()},this._api.addEventListener(n.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(n.CONNECTED,this._connectedHandler),this._api.addEventListener(n.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}}_updateFromStatus(e){e&&(this._status={...this._status,connected:!0,mode:e.status||"running",version:e.version,uptime:e.uptime_seconds||0,activeAgents:e.running_agents||0,pendingTasks:e.pending_tasks||0,phase:e.phase,iteration:e.iteration,complexity:e.complexity},this._state.updateSession({connected:!0,mode:this._status.mode,lastSync:new Date().toISOString()}),this.render())}_startPolling(){this._ownPollInterval=setInterval(async()=>{try{let e=await this._api.getStatus();this._updateFromStatus(e)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}},3e3)}_stopPolling(){this._ownPollInterval&&(clearInterval(this._ownPollInterval),this._ownPollInterval=null)}_formatUptime(e){if(!e||e<0)return"--";let t=Math.floor(e/3600),a=Math.floor(e%3600/60),i=Math.floor(e%60);return t>0?`${t}h ${a}m`:a>0?`${a}m ${i}s`:`${i}s`}_getStatusClass(){switch(this._status.mode){case"running":case"autonomous":return"active";case"paused":return"paused";case"stopped":return"stopped";case"error":return"error";default:return"offline"}}_getStatusLabel(){switch(this._status.mode){case"running":case"autonomous":return"AUTONOMOUS";case"paused":return"PAUSED";case"stopped":return"STOPPED";case"error":return"ERROR";default:return"OFFLINE"}}_triggerStart(){this.dispatchEvent(new CustomEvent("session-start",{detail:this._status}))}async _triggerPause(){try{await this._api.pauseSession(),this._status.mode="paused",this.render()}catch(e){console.error("Failed to pause session:",e)}this.dispatchEvent(new CustomEvent("session-pause",{detail:this._status}))}async _triggerResume(){try{await this._api.resumeSession(),this._status.mode="running",this.render()}catch(e){console.error("Failed to resume session:",e)}this.dispatchEvent(new CustomEvent("session-resume",{detail:this._status}))}async _triggerStop(){try{await this._api.stopSession(),this._status.mode="stopped",this.render()}catch(e){console.error("Failed to stop session:",e)}this.dispatchEvent(new CustomEvent("session-stop",{detail:this._status}))}render(){let e=this.hasAttribute("compact"),t=this._getStatusClass(),a=this._getStatusLabel(),i=["running","autonomous"].includes(this._status.mode),s=this._status.mode==="paused",r=`
|
|
1609
1609
|
<style>
|
|
1610
1610
|
${this.getBaseStyles()}
|
|
1611
1611
|
|
|
@@ -1884,7 +1884,7 @@ var LokiDashboard=(()=>{var K=Object.defineProperty;var pe=Object.getOwnProperty
|
|
|
1884
1884
|
`;this.shadowRoot.innerHTML=`
|
|
1885
1885
|
${r}
|
|
1886
1886
|
${e?o:l}
|
|
1887
|
-
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("pause-btn"),t=this.shadowRoot.getElementById("resume-btn"),a=this.shadowRoot.getElementById("stop-btn"),i=this.shadowRoot.getElementById("start-btn");e&&e.addEventListener("click",()=>this._triggerPause()),t&&t.addEventListener("click",()=>this._triggerResume()),a&&a.addEventListener("click",()=>this._triggerStop()),i&&i.addEventListener("click",()=>this._triggerStart())}};customElements.get("loki-session-control")||customElements.define("loki-session-control",
|
|
1887
|
+
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("pause-btn"),t=this.shadowRoot.getElementById("resume-btn"),a=this.shadowRoot.getElementById("stop-btn"),i=this.shadowRoot.getElementById("start-btn");e&&e.addEventListener("click",()=>this._triggerPause()),t&&t.addEventListener("click",()=>this._triggerResume()),a&&a.addEventListener("click",()=>this._triggerStop()),i&&i.addEventListener("click",()=>this._triggerStart())}};customElements.get("loki-session-control")||customElements.define("loki-session-control",B);var oe={info:{color:"var(--loki-blue)",label:"INFO"},success:{color:"var(--loki-green)",label:"SUCCESS"},warning:{color:"var(--loki-yellow)",label:"WARN"},error:{color:"var(--loki-red)",label:"ERROR"},step:{color:"var(--loki-purple)",label:"STEP"},agent:{color:"var(--loki-accent)",label:"AGENT"},debug:{color:"var(--loki-text-muted)",label:"DEBUG"}},U=class extends c{static get observedAttributes(){return["api-url","max-lines","auto-scroll","theme","log-file"]}constructor(){super(),this._logs=[],this._maxLines=500,this._autoScroll=!0,this._filter="",this._levelFilter="all",this._api=null,this._pollInterval=null,this._logMessageHandler=null}connectedCallback(){super.connectedCallback(),this._maxLines=parseInt(this.getAttribute("max-lines"))||500,this._autoScroll=this.hasAttribute("auto-scroll"),this._setupApi(),this._startLogPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopLogPolling(),this._api&&this._logMessageHandler&&this._api.removeEventListener(n.LOG_MESSAGE,this._logMessageHandler)}attributeChangedCallback(e,t,a){if(t!==a)switch(e){case"api-url":this._api&&(this._api.baseUrl=a);break;case"max-lines":this._maxLines=parseInt(a)||500,this._trimLogs(),this.render();break;case"auto-scroll":this._autoScroll=this.hasAttribute("auto-scroll"),this.render();break;case"theme":this._applyTheme();break}}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=u({baseUrl:e}),this._logMessageHandler=t=>this._addLog(t.detail),this._api.addEventListener(n.LOG_MESSAGE,this._logMessageHandler)}_startLogPolling(){let e=this.getAttribute("log-file");e?this._pollLogFile(e):this._pollApiLogs()}async _pollApiLogs(){let e=0,t=async()=>{try{let a=await this._api.getLogs(200);if(Array.isArray(a)&&a.length>e){let i=a.slice(e);for(let s of i)s.message&&s.message.trim()&&this._addLog({message:s.message,level:s.level||"info",timestamp:s.timestamp||new Date().toLocaleTimeString()});e=a.length}}catch{}};t(),this._apiPollInterval=setInterval(t,2e3)}async _pollLogFile(e){let t=0,a=async()=>{try{let i=await fetch(`${e}?t=${Date.now()}`);if(!i.ok)return;let r=(await i.text()).split(`
|
|
1888
1888
|
`);if(r.length>t){let o=r.slice(t);for(let l of o)l.trim()&&this._addLog(this._parseLine(l));t=r.length}}catch{}};a(),this._pollInterval=setInterval(a,1e3)}_stopLogPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._apiPollInterval&&(clearInterval(this._apiPollInterval),this._apiPollInterval=null)}_parseLine(e){let t=e.match(/^\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.+)$/);if(t)return{timestamp:t[1],level:t[2].toLowerCase(),message:t[3]};let a=e.match(/^(\d{2}:\d{2}:\d{2})\s+(\w+)\s+(.+)$/);return a?{timestamp:a[1],level:a[2].toLowerCase(),message:a[3]}:{timestamp:new Date().toLocaleTimeString(),level:"info",message:e}}_addLog(e){if(!e)return;let t={id:Date.now()+Math.random(),timestamp:e.timestamp||new Date().toLocaleTimeString(),level:(e.level||"info").toLowerCase(),message:e.message||e};this._logs.push(t),this._trimLogs(),this.dispatchEvent(new CustomEvent("log-received",{detail:t})),this._renderLogs(),this._autoScroll&&this._scrollToBottom()}_trimLogs(){this._logs.length>this._maxLines&&(this._logs=this._logs.slice(-this._maxLines))}_clearLogs(){this._logs=[],this.dispatchEvent(new CustomEvent("logs-cleared")),this._renderLogs()}_toggleAutoScroll(){this._autoScroll=!this._autoScroll,this.render(),this._autoScroll&&this._scrollToBottom()}_scrollToBottom(){requestAnimationFrame(()=>{let e=this.shadowRoot.getElementById("log-output");e&&(e.scrollTop=e.scrollHeight)})}_downloadLogs(){let e=this._logs.map(s=>`[${s.timestamp}] [${s.level.toUpperCase()}] ${s.message}`).join(`
|
|
1889
1889
|
`),t=new Blob([e],{type:"text/plain"}),a=URL.createObjectURL(t),i=document.createElement("a");i.href=a,i.download=`loki-logs-${new Date().toISOString().split("T")[0]}.txt`,i.click(),URL.revokeObjectURL(a)}_setFilter(e){this._filter=e.toLowerCase(),this._renderLogs()}_setLevelFilter(e){this._levelFilter=e,this._renderLogs()}_getFilteredLogs(){return this._logs.filter(e=>!(this._levelFilter!=="all"&&e.level!==this._levelFilter||this._filter&&!e.message.toLowerCase().includes(this._filter)))}_renderLogs(){let e=this.shadowRoot.getElementById("log-output");if(!e)return;let t=this._getFilteredLogs();if(t.length===0){e.innerHTML='<div class="log-empty">No log output yet. Terminal will update when Loki Mode is running.</div>';return}e.innerHTML=t.map(a=>{let i=oe[a.level]||oe.info;return`
|
|
1890
1890
|
<div class="log-line">
|
package/dashboard/telemetry.py
CHANGED
|
@@ -33,7 +33,10 @@ def _get_distinct_id():
|
|
|
33
33
|
except Exception:
|
|
34
34
|
new_id = str(uuid.uuid4())
|
|
35
35
|
try:
|
|
36
|
-
|
|
36
|
+
# Create with 0600 permissions (user read/write only)
|
|
37
|
+
fd = os.open(str(id_file), os.O_CREAT | os.O_WRONLY, 0o600)
|
|
38
|
+
os.write(fd, (new_id + "\n").encode())
|
|
39
|
+
os.close(fd)
|
|
37
40
|
except Exception:
|
|
38
41
|
pass
|
|
39
42
|
return new_id
|
package/docs/INSTALLATION.md
CHANGED
package/events/emit.sh
CHANGED
|
@@ -29,7 +29,17 @@ if [ "$#" -ge 3 ]; then shift 3; else shift "$#"; fi
|
|
|
29
29
|
|
|
30
30
|
# Generate event ID and timestamp
|
|
31
31
|
EVENT_ID=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
32
|
-
|
|
32
|
+
# Try GNU date %N (nanoseconds) first, fall back to python3, then .000Z
|
|
33
|
+
if date --version >/dev/null 2>&1; then
|
|
34
|
+
# GNU date (Linux)
|
|
35
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
36
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
# macOS fallback: use python3 for milliseconds
|
|
38
|
+
TIMESTAMP=$(python3 -c "from datetime import datetime, timezone; print(datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z')")
|
|
39
|
+
else
|
|
40
|
+
# Final fallback: .000Z
|
|
41
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
42
|
+
fi
|
|
33
43
|
|
|
34
44
|
# JSON escape helper: handles \, ", and control characters
|
|
35
45
|
json_escape() {
|
|
@@ -67,5 +77,16 @@ EOF
|
|
|
67
77
|
EVENT_FILE="$EVENTS_DIR/${TIMESTAMP//:/-}_$EVENT_ID.json"
|
|
68
78
|
echo "$EVENT" > "$EVENT_FILE"
|
|
69
79
|
|
|
80
|
+
# Rotate events.jsonl if it exceeds 50MB (keep 1 backup)
|
|
81
|
+
EVENTS_LOG="$LOKI_DIR/events.jsonl"
|
|
82
|
+
if [ -f "$EVENTS_LOG" ]; then
|
|
83
|
+
# Check file size (in bytes)
|
|
84
|
+
FILE_SIZE=$(stat -f%z "$EVENTS_LOG" 2>/dev/null || stat -c%s "$EVENTS_LOG" 2>/dev/null || echo 0)
|
|
85
|
+
MAX_SIZE=$((50 * 1024 * 1024)) # 50MB
|
|
86
|
+
if [ "$FILE_SIZE" -gt "$MAX_SIZE" ]; then
|
|
87
|
+
mv "$EVENTS_LOG" "$EVENTS_LOG.1" 2>/dev/null || true
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
70
91
|
# Output event ID
|
|
71
92
|
echo "$EVENT_ID"
|
package/mcp/__init__.py
CHANGED