loki-mode 6.37.2 → 6.37.4
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 +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +64 -21
- package/autonomy/run.sh +6 -5
- package/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +54 -29
- package/dashboard/server.py +163 -50
- package/dashboard/static/index.html +3 -3
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/server.py +38 -16
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 minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.37.
|
|
6
|
+
# Loki Mode v6.37.4
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.37.
|
|
270
|
+
**v6.37.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.37.
|
|
1
|
+
6.37.4
|
package/autonomy/loki
CHANGED
|
@@ -7788,6 +7788,36 @@ print(json.dumps({'id': manifest.id, 'dir': str(pipeline.migration_dir)}))
|
|
|
7788
7788
|
phases_to_run=("understand" "guardrail" "migrate" "verify")
|
|
7789
7789
|
fi
|
|
7790
7790
|
|
|
7791
|
+
# Validate prerequisite artifacts exist before running phases (#81)
|
|
7792
|
+
for p in "${phases_to_run[@]}"; do
|
|
7793
|
+
case "$p" in
|
|
7794
|
+
guardrail)
|
|
7795
|
+
if [ ! -f "${migration_dir}/docs/analysis.md" ] || [ ! -f "${migration_dir}/seams.json" ]; then
|
|
7796
|
+
echo -e "${RED}Error: Phase 'guardrail' requires artifacts from 'understand' phase${NC}"
|
|
7797
|
+
echo -e "${DIM} Missing: analysis.md or seams.json in ${migration_dir}${NC}"
|
|
7798
|
+
echo "Run: loki migrate ${codebase_path} --target ${target} --phase understand"
|
|
7799
|
+
return 1
|
|
7800
|
+
fi
|
|
7801
|
+
;;
|
|
7802
|
+
migrate)
|
|
7803
|
+
if [ ! -f "${migration_dir}/features.json" ]; then
|
|
7804
|
+
echo -e "${RED}Error: Phase 'migrate' requires artifacts from 'guardrail' phase${NC}"
|
|
7805
|
+
echo -e "${DIM} Missing: features.json in ${migration_dir}${NC}"
|
|
7806
|
+
echo "Run: loki migrate ${codebase_path} --target ${target} --phase guardrail"
|
|
7807
|
+
return 1
|
|
7808
|
+
fi
|
|
7809
|
+
;;
|
|
7810
|
+
verify)
|
|
7811
|
+
if [ ! -f "${migration_dir}/migration-plan.json" ]; then
|
|
7812
|
+
echo -e "${RED}Error: Phase 'verify' requires migration-plan.json from 'migrate' phase${NC}"
|
|
7813
|
+
echo -e "${DIM} Missing: migration-plan.json in ${migration_dir}${NC}"
|
|
7814
|
+
echo "Run: loki migrate ${codebase_path} --target ${target} --phase migrate"
|
|
7815
|
+
return 1
|
|
7816
|
+
fi
|
|
7817
|
+
;;
|
|
7818
|
+
esac
|
|
7819
|
+
done
|
|
7820
|
+
|
|
7791
7821
|
# Execute phases
|
|
7792
7822
|
for p in "${phases_to_run[@]}"; do
|
|
7793
7823
|
echo -e "${CYAN}[Phase: ${p}]${NC} Starting..."
|
|
@@ -12595,24 +12625,32 @@ cmd_council() {
|
|
|
12595
12625
|
|
|
12596
12626
|
if [ -f "$council_dir/state.json" ]; then
|
|
12597
12627
|
LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
|
|
12598
|
-
import json, os
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
print(f\"
|
|
12603
|
-
print(f\"
|
|
12604
|
-
print(f\"
|
|
12605
|
-
print(f\"
|
|
12606
|
-
print(f\"
|
|
12607
|
-
print(f\"
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
"
|
|
12628
|
+
import json, os, sys
|
|
12629
|
+
try:
|
|
12630
|
+
with open(os.environ['LOKI_COUNCIL_STATE']) as f:
|
|
12631
|
+
state = json.load(f)
|
|
12632
|
+
print(f\"Enabled: {state.get('initialized', False)}\")
|
|
12633
|
+
print(f\"Total votes: {state.get('total_votes', 0)}\")
|
|
12634
|
+
print(f\"Approve votes: {state.get('approve_votes', 0)}\")
|
|
12635
|
+
print(f\"Reject votes: {state.get('reject_votes', 0)}\")
|
|
12636
|
+
print(f\"Stagnation streak: {state.get('consecutive_no_change', 0)}\")
|
|
12637
|
+
print(f\"Done signals: {state.get('done_signals', 0)}\")
|
|
12638
|
+
print(f\"Last check: iteration {state.get('last_check_iteration', 'none')}\")
|
|
12639
|
+
verdicts = state.get('verdicts', [])
|
|
12640
|
+
if verdicts:
|
|
12641
|
+
print(f\"\nRecent verdicts:\")
|
|
12642
|
+
for v in verdicts[-5:]:
|
|
12643
|
+
print(f\" Iteration {v['iteration']}: {v['result']} ({v['approve']} approve / {v['reject']} reject)\")
|
|
12644
|
+
else:
|
|
12645
|
+
print(f\"\nNo verdicts yet\")
|
|
12646
|
+
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
12647
|
+
print(f'Error: Council state file is corrupted or invalid: {e}', file=sys.stderr)
|
|
12648
|
+
print('Delete .loki/council/state.json and restart the session to reset.')
|
|
12649
|
+
sys.exit(1)
|
|
12650
|
+
except Exception as e:
|
|
12651
|
+
print(f'Error reading council state: {e}', file=sys.stderr)
|
|
12652
|
+
sys.exit(1)
|
|
12653
|
+
" 2>&1
|
|
12616
12654
|
else
|
|
12617
12655
|
echo "Council state file not found"
|
|
12618
12656
|
fi
|
|
@@ -12623,9 +12661,14 @@ else:
|
|
|
12623
12661
|
|
|
12624
12662
|
if [ -f "$council_dir/state.json" ]; then
|
|
12625
12663
|
LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
|
|
12626
|
-
import json, os
|
|
12627
|
-
|
|
12628
|
-
|
|
12664
|
+
import json, os, sys
|
|
12665
|
+
try:
|
|
12666
|
+
with open(os.environ['LOKI_COUNCIL_STATE']) as f:
|
|
12667
|
+
state = json.load(f)
|
|
12668
|
+
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
12669
|
+
print(f'Error: Council state file is corrupted: {e}', file=sys.stderr)
|
|
12670
|
+
print('Delete .loki/council/state.json and restart the session to reset.')
|
|
12671
|
+
sys.exit(1)
|
|
12629
12672
|
verdicts = state.get('verdicts', [])
|
|
12630
12673
|
if not verdicts:
|
|
12631
12674
|
print('No decisions recorded yet')
|
package/autonomy/run.sh
CHANGED
|
@@ -5502,10 +5502,11 @@ run_code_review() {
|
|
|
5502
5502
|
log_info "Selecting 3 specialist reviewers from pool..."
|
|
5503
5503
|
|
|
5504
5504
|
# Write diff/files to temp files for python to read (avoid env var size limits)
|
|
5505
|
+
# Use printf to prevent shell variable expansion in diff content (#78)
|
|
5505
5506
|
local diff_file="$review_dir/$review_id/diff.txt"
|
|
5506
5507
|
local files_file="$review_dir/$review_id/files.txt"
|
|
5507
|
-
|
|
5508
|
-
|
|
5508
|
+
printf '%s\n' "$diff_content" > "$diff_file"
|
|
5509
|
+
printf '%s\n' "$changed_files" > "$files_file"
|
|
5509
5510
|
|
|
5510
5511
|
# Select specialists via keyword scoring (python3 reads files, not env vars)
|
|
5511
5512
|
# Loads from agents/types.json when available, falls back to hardcoded pool (v6.7.0)
|
|
@@ -5881,11 +5882,11 @@ run_adversarial_testing() {
|
|
|
5881
5882
|
local changed_files
|
|
5882
5883
|
changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
|
|
5883
5884
|
|
|
5884
|
-
# Write analysis files
|
|
5885
|
+
# Write analysis files -- use printf to prevent shell variable expansion (#78)
|
|
5885
5886
|
local diff_file="$adversarial_dir/$test_id/diff.txt"
|
|
5886
5887
|
local files_file="$adversarial_dir/$test_id/files.txt"
|
|
5887
|
-
|
|
5888
|
-
|
|
5888
|
+
printf '%s\n' "$diff_content" > "$diff_file"
|
|
5889
|
+
printf '%s\n' "$changed_files" > "$files_file"
|
|
5889
5890
|
|
|
5890
5891
|
# Build adversarial prompt -- use heredoc with quoted delimiter to prevent
|
|
5891
5892
|
# shell variable expansion in diff content (fixes #78)
|
package/dashboard/__init__.py
CHANGED
package/dashboard/auth.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
16
|
import hashlib
|
|
17
|
+
import hmac
|
|
17
18
|
import json
|
|
18
19
|
import os
|
|
19
20
|
import secrets
|
|
@@ -58,17 +59,29 @@ _SCOPE_HIERARCHY = {
|
|
|
58
59
|
if OIDC_ENABLED:
|
|
59
60
|
import logging as _logging
|
|
60
61
|
_logger = _logging.getLogger("loki.auth")
|
|
62
|
+
_pyjwt_available = False
|
|
63
|
+
try:
|
|
64
|
+
import jwt as _pyjwt_check # noqa: F401
|
|
65
|
+
from jwt import PyJWKClient as _PyJWKClient_check # noqa: F401
|
|
66
|
+
_pyjwt_available = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
_pyjwt_available = False
|
|
69
|
+
|
|
61
70
|
if OIDC_SKIP_SIGNATURE_VERIFY:
|
|
62
71
|
_logger.critical(
|
|
63
72
|
"OIDC/SSO signature verification DISABLED (LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true). "
|
|
64
73
|
"This is INSECURE and allows forged JWTs. Only use for local testing. "
|
|
65
74
|
"For production, install PyJWT + cryptography and remove this env var."
|
|
66
75
|
)
|
|
76
|
+
elif _pyjwt_available:
|
|
77
|
+
_logger.info(
|
|
78
|
+
"OIDC/SSO enabled with PyJWT cryptographic signature verification (RS256/RS384/RS512)."
|
|
79
|
+
)
|
|
67
80
|
else:
|
|
68
|
-
_logger.
|
|
69
|
-
"OIDC/SSO enabled
|
|
70
|
-
"
|
|
71
|
-
"cryptography
|
|
81
|
+
_logger.critical(
|
|
82
|
+
"OIDC/SSO enabled but PyJWT is NOT installed. Tokens will be REJECTED "
|
|
83
|
+
"unless LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true is set. "
|
|
84
|
+
"Install PyJWT + cryptography: pip install PyJWT cryptography"
|
|
72
85
|
)
|
|
73
86
|
|
|
74
87
|
# OIDC JWKS cache (issuer URL -> (keys_dict, fetch_timestamp))
|
|
@@ -103,6 +116,9 @@ def _save_tokens(tokens: dict) -> None:
|
|
|
103
116
|
TOKEN_FILE.touch(mode=0o600, exist_ok=True)
|
|
104
117
|
with open(TOKEN_FILE, "w") as f:
|
|
105
118
|
json.dump(tokens, f, indent=2, default=str)
|
|
119
|
+
# Enforce 0600 on every write, not just creation -- touch(mode=) only
|
|
120
|
+
# applies when the file is new, so an external chmod would persist.
|
|
121
|
+
os.chmod(TOKEN_FILE, 0o600)
|
|
106
122
|
|
|
107
123
|
|
|
108
124
|
def _hash_token(token: str, salt: str = None) -> tuple[str, str]:
|
|
@@ -123,7 +139,7 @@ def _hash_token(token: str, salt: str = None) -> tuple[str, str]:
|
|
|
123
139
|
|
|
124
140
|
def _constant_time_compare(a: str, b: str) -> bool:
|
|
125
141
|
"""Constant-time string comparison to prevent timing attacks."""
|
|
126
|
-
return
|
|
142
|
+
return hmac.compare_digest(a.encode(), b.encode())
|
|
127
143
|
|
|
128
144
|
|
|
129
145
|
def resolve_scopes(role_or_scopes) -> list[str]:
|
|
@@ -342,30 +358,35 @@ def validate_token(raw_token: str) -> Optional[dict]:
|
|
|
342
358
|
|
|
343
359
|
tokens = _load_tokens()
|
|
344
360
|
|
|
345
|
-
#
|
|
361
|
+
# Iterate ALL tokens to prevent timing side-channel that leaks token count.
|
|
362
|
+
# Do not short-circuit on match -- always hash and compare every entry.
|
|
363
|
+
matched_token: Optional[dict] = None
|
|
346
364
|
for token in tokens["tokens"].values():
|
|
347
365
|
stored_salt = token.get("salt", "")
|
|
348
366
|
token_hash, _ = _hash_token(raw_token, salt=stored_salt)
|
|
349
367
|
if _constant_time_compare(token["hash"], token_hash):
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
368
|
+
matched_token = token
|
|
369
|
+
|
|
370
|
+
if matched_token is not None:
|
|
371
|
+
# Check if revoked
|
|
372
|
+
if matched_token.get("revoked"):
|
|
373
|
+
return None
|
|
353
374
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
375
|
+
# Check expiration
|
|
376
|
+
if matched_token.get("expires_at"):
|
|
377
|
+
expires = datetime.fromisoformat(matched_token["expires_at"])
|
|
378
|
+
if datetime.now(timezone.utc) > expires:
|
|
379
|
+
return None
|
|
359
380
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
381
|
+
# Update last used
|
|
382
|
+
matched_token["last_used"] = datetime.now(timezone.utc).isoformat()
|
|
383
|
+
_save_tokens(tokens)
|
|
363
384
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
return {
|
|
386
|
+
"id": matched_token["id"],
|
|
387
|
+
"name": matched_token["name"],
|
|
388
|
+
"scopes": matched_token["scopes"],
|
|
389
|
+
}
|
|
369
390
|
|
|
370
391
|
return None
|
|
371
392
|
|
|
@@ -517,14 +538,18 @@ def validate_oidc_token(token_str: str) -> Optional[dict]:
|
|
|
517
538
|
"issuer": decoded.get("iss"),
|
|
518
539
|
}
|
|
519
540
|
except ImportError:
|
|
520
|
-
# PyJWT not installed --
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
541
|
+
# PyJWT not installed -- only allow claims-only path if explicitly opted in
|
|
542
|
+
if not OIDC_SKIP_SIGNATURE_VERIFY:
|
|
543
|
+
_auth_logger.error(
|
|
544
|
+
"OIDC token rejected: PyJWT not installed and "
|
|
545
|
+
"LOKI_OIDC_SKIP_SIGNATURE_VERIFY is not set. "
|
|
546
|
+
"Install PyJWT: pip install PyJWT cryptography"
|
|
547
|
+
)
|
|
548
|
+
return None
|
|
549
|
+
_auth_logger.warning(
|
|
550
|
+
"PyJWT not installed -- using claims-only validation "
|
|
551
|
+
"(LOKI_OIDC_SKIP_SIGNATURE_VERIFY=true). This is INSECURE."
|
|
524
552
|
)
|
|
525
|
-
_auth_logger.warning(_warning_msg)
|
|
526
|
-
# Also print to stderr so operators notice even without log config
|
|
527
|
-
print(_warning_msg, file=sys.stderr)
|
|
528
553
|
except Exception as exc:
|
|
529
554
|
_auth_logger.error("PyJWT signature verification failed: %s", exc)
|
|
530
555
|
return None
|
package/dashboard/server.py
CHANGED
|
@@ -30,7 +30,7 @@ from fastapi import (
|
|
|
30
30
|
)
|
|
31
31
|
from fastapi.middleware.cors import CORSMiddleware
|
|
32
32
|
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
33
|
-
from pydantic import BaseModel, Field
|
|
33
|
+
from pydantic import BaseModel, Field, field_validator
|
|
34
34
|
from sqlalchemy import select, update, delete
|
|
35
35
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
36
36
|
from sqlalchemy.orm import selectinload
|
|
@@ -75,6 +75,27 @@ def _safe_int_env(name: str, default: int) -> int:
|
|
|
75
75
|
return default
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _safe_json_read(path: _Path, default: Any = None) -> Any:
|
|
79
|
+
"""Read a JSON file with retry on partial/corrupt data from concurrent writes."""
|
|
80
|
+
for attempt in range(2):
|
|
81
|
+
try:
|
|
82
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
83
|
+
return json.loads(text)
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
if attempt == 0:
|
|
86
|
+
time.sleep(0.1)
|
|
87
|
+
continue
|
|
88
|
+
return default
|
|
89
|
+
except (OSError, IOError):
|
|
90
|
+
return default
|
|
91
|
+
return default
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _safe_read_text(path: _Path) -> str:
|
|
95
|
+
"""Read a text file with UTF-8 encoding, replacing non-UTF-8 bytes."""
|
|
96
|
+
return open(path, encoding="utf-8", errors="replace").read()
|
|
97
|
+
|
|
98
|
+
|
|
78
99
|
# ---------------------------------------------------------------------------
|
|
79
100
|
# Simple in-memory rate limiter for control endpoints
|
|
80
101
|
# ---------------------------------------------------------------------------
|
|
@@ -98,12 +119,12 @@ class _RateLimiter:
|
|
|
98
119
|
for k in empty_keys:
|
|
99
120
|
del self._calls[k]
|
|
100
121
|
|
|
101
|
-
# Evict
|
|
122
|
+
# Evict least-recently-accessed keys if max_keys exceeded
|
|
102
123
|
if len(self._calls) > self._max_keys:
|
|
103
|
-
# Sort by
|
|
124
|
+
# Sort by last-access time (most recent timestamp), evict least recent
|
|
104
125
|
sorted_keys = sorted(
|
|
105
126
|
self._calls.items(),
|
|
106
|
-
key=lambda x:
|
|
127
|
+
key=lambda x: max(x[1]) if x[1] else 0
|
|
107
128
|
)
|
|
108
129
|
keys_to_remove = len(self._calls) - self._max_keys
|
|
109
130
|
for k, _ in sorted_keys[:keys_to_remove]:
|
|
@@ -123,12 +144,30 @@ logger = logging.getLogger(__name__)
|
|
|
123
144
|
|
|
124
145
|
|
|
125
146
|
# Pydantic schemas for API
|
|
147
|
+
def _sanitize_text_field(value: str) -> str:
|
|
148
|
+
"""Strip/reject control characters from text fields."""
|
|
149
|
+
import unicodedata
|
|
150
|
+
# Remove control characters (except common whitespace like space)
|
|
151
|
+
cleaned = "".join(
|
|
152
|
+
ch for ch in value if unicodedata.category(ch)[0] != "C" or ch in (" ",)
|
|
153
|
+
)
|
|
154
|
+
cleaned = cleaned.strip()
|
|
155
|
+
if not cleaned:
|
|
156
|
+
raise ValueError("Field must not be empty after removing control characters")
|
|
157
|
+
return cleaned
|
|
158
|
+
|
|
159
|
+
|
|
126
160
|
class ProjectCreate(BaseModel):
|
|
127
161
|
"""Schema for creating a project."""
|
|
128
162
|
name: str = Field(..., min_length=1, max_length=255)
|
|
129
163
|
description: Optional[str] = None
|
|
130
164
|
prd_path: Optional[str] = None
|
|
131
165
|
|
|
166
|
+
@field_validator("name")
|
|
167
|
+
@classmethod
|
|
168
|
+
def validate_name(cls, v: str) -> str:
|
|
169
|
+
return _sanitize_text_field(v)
|
|
170
|
+
|
|
132
171
|
|
|
133
172
|
class ProjectUpdate(BaseModel):
|
|
134
173
|
"""Schema for updating a project."""
|
|
@@ -165,6 +204,11 @@ class TaskCreate(BaseModel):
|
|
|
165
204
|
parent_task_id: Optional[int] = None
|
|
166
205
|
estimated_duration: Optional[int] = None
|
|
167
206
|
|
|
207
|
+
@field_validator("title")
|
|
208
|
+
@classmethod
|
|
209
|
+
def validate_title(cls, v: str) -> str:
|
|
210
|
+
return _sanitize_text_field(v)
|
|
211
|
+
|
|
168
212
|
|
|
169
213
|
class TaskUpdate(BaseModel):
|
|
170
214
|
"""Schema for updating a task."""
|
|
@@ -315,7 +359,7 @@ async def _push_loki_state_loop() -> None:
|
|
|
315
359
|
if mtime != last_mtime:
|
|
316
360
|
last_mtime = mtime
|
|
317
361
|
try:
|
|
318
|
-
raw =
|
|
362
|
+
raw = _safe_json_read(state_file, {})
|
|
319
363
|
# Transform to StatusResponse-compatible format
|
|
320
364
|
agents_list = raw.get("agents", [])
|
|
321
365
|
running_agents = len(agents_list) if isinstance(agents_list, list) else 0
|
|
@@ -515,10 +559,10 @@ async def get_status() -> StatusResponse:
|
|
|
515
559
|
pending_tasks = 0
|
|
516
560
|
running_agents = 0
|
|
517
561
|
|
|
518
|
-
# Read dashboard state
|
|
562
|
+
# Read dashboard state (with retry for concurrent writes)
|
|
519
563
|
if state_file.exists():
|
|
520
564
|
try:
|
|
521
|
-
state =
|
|
565
|
+
state = _safe_json_read(state_file, {})
|
|
522
566
|
phase = state.get("phase", "")
|
|
523
567
|
iteration = state.get("iteration", 0)
|
|
524
568
|
complexity = state.get("complexity", "standard")
|
|
@@ -561,7 +605,7 @@ async def get_status() -> StatusResponse:
|
|
|
561
605
|
# Also check session.json for skill-invoked sessions
|
|
562
606
|
if not running and session_file.exists():
|
|
563
607
|
try:
|
|
564
|
-
sd =
|
|
608
|
+
sd = _safe_json_read(session_file, {})
|
|
565
609
|
if sd.get("status") == "running":
|
|
566
610
|
running = True
|
|
567
611
|
except (json.JSONDecodeError, KeyError):
|
|
@@ -673,21 +717,41 @@ async def get_status() -> StatusResponse:
|
|
|
673
717
|
@app.get("/api/projects", response_model=list[ProjectResponse])
|
|
674
718
|
async def list_projects(
|
|
675
719
|
status: Optional[str] = Query(None),
|
|
720
|
+
limit: int = Query(default=50, ge=1, le=500),
|
|
721
|
+
offset: int = Query(default=0, ge=0),
|
|
676
722
|
db: AsyncSession = Depends(get_db),
|
|
677
723
|
) -> list[ProjectResponse]:
|
|
678
|
-
"""List
|
|
679
|
-
|
|
724
|
+
"""List projects with pagination. Does not eager-load tasks for efficiency."""
|
|
725
|
+
from sqlalchemy import func as sa_func
|
|
726
|
+
|
|
727
|
+
query = select(Project)
|
|
680
728
|
if status:
|
|
681
729
|
query = query.where(Project.status == status)
|
|
682
|
-
query = query.order_by(Project.created_at.desc())
|
|
730
|
+
query = query.order_by(Project.created_at.desc()).offset(offset).limit(limit)
|
|
683
731
|
|
|
684
732
|
result = await db.execute(query)
|
|
685
733
|
projects = result.scalars().all()
|
|
686
734
|
|
|
735
|
+
# Batch-fetch task counts instead of N+1 eager loading
|
|
736
|
+
project_ids = [p.id for p in projects]
|
|
687
737
|
response = []
|
|
738
|
+
if project_ids:
|
|
739
|
+
count_query = (
|
|
740
|
+
select(
|
|
741
|
+
Task.project_id,
|
|
742
|
+
sa_func.count().label("total"),
|
|
743
|
+
sa_func.count().filter(Task.status == TaskStatus.DONE).label("done"),
|
|
744
|
+
)
|
|
745
|
+
.where(Task.project_id.in_(project_ids))
|
|
746
|
+
.group_by(Task.project_id)
|
|
747
|
+
)
|
|
748
|
+
count_result = await db.execute(count_query)
|
|
749
|
+
counts = {row.project_id: (row.total, row.done) for row in count_result}
|
|
750
|
+
else:
|
|
751
|
+
counts = {}
|
|
752
|
+
|
|
688
753
|
for project in projects:
|
|
689
|
-
|
|
690
|
-
completed_count = len([t for t in project.tasks if t.status == TaskStatus.DONE])
|
|
754
|
+
total, done = counts.get(project.id, (0, 0))
|
|
691
755
|
response.append(
|
|
692
756
|
ProjectResponse(
|
|
693
757
|
id=project.id,
|
|
@@ -697,8 +761,8 @@ async def list_projects(
|
|
|
697
761
|
status=project.status,
|
|
698
762
|
created_at=project.created_at,
|
|
699
763
|
updated_at=project.updated_at,
|
|
700
|
-
task_count=
|
|
701
|
-
completed_task_count=
|
|
764
|
+
task_count=total,
|
|
765
|
+
completed_task_count=done,
|
|
702
766
|
)
|
|
703
767
|
)
|
|
704
768
|
return response
|
|
@@ -1066,12 +1130,29 @@ async def create_task(
|
|
|
1066
1130
|
Task.project_id == task.project_id
|
|
1067
1131
|
)
|
|
1068
1132
|
)
|
|
1069
|
-
|
|
1133
|
+
parent = result.scalar_one_or_none()
|
|
1134
|
+
if not parent:
|
|
1070
1135
|
raise HTTPException(
|
|
1071
1136
|
status_code=400,
|
|
1072
1137
|
detail="Parent task not found or belongs to different project"
|
|
1073
1138
|
)
|
|
1074
1139
|
|
|
1140
|
+
# Detect circular reference: walk parent chain
|
|
1141
|
+
visited = set()
|
|
1142
|
+
current_parent_id = task.parent_task_id
|
|
1143
|
+
while current_parent_id is not None:
|
|
1144
|
+
if current_parent_id in visited:
|
|
1145
|
+
raise HTTPException(
|
|
1146
|
+
status_code=422,
|
|
1147
|
+
detail="Circular reference detected in parent task chain"
|
|
1148
|
+
)
|
|
1149
|
+
visited.add(current_parent_id)
|
|
1150
|
+
parent_result = await db.execute(
|
|
1151
|
+
select(Task.parent_task_id).where(Task.id == current_parent_id)
|
|
1152
|
+
)
|
|
1153
|
+
row = parent_result.scalar_one_or_none()
|
|
1154
|
+
current_parent_id = row if row else None
|
|
1155
|
+
|
|
1075
1156
|
db_task = Task(
|
|
1076
1157
|
project_id=task.project_id,
|
|
1077
1158
|
title=task.title,
|
|
@@ -1192,6 +1273,15 @@ async def delete_task(
|
|
|
1192
1273
|
})
|
|
1193
1274
|
|
|
1194
1275
|
|
|
1276
|
+
# Valid status transitions for task state machine
|
|
1277
|
+
_TASK_STATE_MACHINE: dict[TaskStatus, set[TaskStatus]] = {
|
|
1278
|
+
TaskStatus.BACKLOG: {TaskStatus.PENDING},
|
|
1279
|
+
TaskStatus.PENDING: {TaskStatus.IN_PROGRESS},
|
|
1280
|
+
TaskStatus.IN_PROGRESS: {TaskStatus.REVIEW, TaskStatus.DONE},
|
|
1281
|
+
TaskStatus.REVIEW: {TaskStatus.DONE, TaskStatus.IN_PROGRESS},
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
|
|
1195
1285
|
@app.post("/api/tasks/{task_id}/move", response_model=TaskResponse, dependencies=[Depends(auth.require_scope("control"))])
|
|
1196
1286
|
async def move_task(
|
|
1197
1287
|
task_id: int,
|
|
@@ -1208,6 +1298,18 @@ async def move_task(
|
|
|
1208
1298
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1209
1299
|
|
|
1210
1300
|
old_status = task.status
|
|
1301
|
+
|
|
1302
|
+
# Validate status transition
|
|
1303
|
+
if move.status != old_status:
|
|
1304
|
+
allowed = _TASK_STATE_MACHINE.get(old_status, set())
|
|
1305
|
+
if move.status not in allowed:
|
|
1306
|
+
raise HTTPException(
|
|
1307
|
+
status_code=422,
|
|
1308
|
+
detail=f"Invalid status transition: {old_status.value} -> {move.status.value}. "
|
|
1309
|
+
f"Allowed transitions from {old_status.value}: "
|
|
1310
|
+
f"{', '.join(s.value for s in allowed) if allowed else 'none'}",
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1211
1313
|
task.status = move.status
|
|
1212
1314
|
task.position = move.position
|
|
1213
1315
|
|
|
@@ -1252,8 +1354,9 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
1252
1354
|
# proxy access logs -- configure log sanitization for /ws in production.
|
|
1253
1355
|
# FastAPI Depends() is not supported on @app.websocket() routes.
|
|
1254
1356
|
|
|
1255
|
-
# Rate limit WebSocket connections by IP
|
|
1256
|
-
|
|
1357
|
+
# Rate limit WebSocket connections by IP (use unique key when client info unavailable)
|
|
1358
|
+
import uuid as _uuid
|
|
1359
|
+
client_ip = websocket.client.host if websocket.client else f"ws-{_uuid.uuid4().hex}"
|
|
1257
1360
|
if not _read_limiter.check(f"ws_{client_ip}"):
|
|
1258
1361
|
await websocket.close(code=1008) # Policy Violation
|
|
1259
1362
|
return
|
|
@@ -1284,20 +1387,23 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
1284
1387
|
"data": {"message": "Connected to Loki Dashboard"},
|
|
1285
1388
|
})
|
|
1286
1389
|
|
|
1287
|
-
# Keep connection alive and handle incoming messages
|
|
1390
|
+
# Keep connection alive and handle incoming messages.
|
|
1391
|
+
# Close idle connections after ~60s of no response to pings.
|
|
1392
|
+
missed_pongs = 0
|
|
1288
1393
|
while True:
|
|
1289
1394
|
try:
|
|
1290
1395
|
data = await asyncio.wait_for(
|
|
1291
1396
|
websocket.receive_text(),
|
|
1292
|
-
timeout=30.0 # Ping every 30 seconds
|
|
1397
|
+
timeout=30.0 # Ping every 30 seconds of silence
|
|
1293
1398
|
)
|
|
1294
|
-
#
|
|
1399
|
+
missed_pongs = 0 # any message resets idle counter
|
|
1295
1400
|
try:
|
|
1296
1401
|
message = json.loads(data)
|
|
1297
1402
|
if message.get("type") == "ping":
|
|
1298
1403
|
await manager.send_personal(websocket, {"type": "pong"})
|
|
1404
|
+
elif message.get("type") == "pong":
|
|
1405
|
+
pass # client responded to our ping
|
|
1299
1406
|
elif message.get("type") == "subscribe":
|
|
1300
|
-
# Could implement channel subscriptions here
|
|
1301
1407
|
await manager.send_personal(websocket, {
|
|
1302
1408
|
"type": "subscribed",
|
|
1303
1409
|
"data": message.get("data", {}),
|
|
@@ -1305,8 +1411,15 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
|
1305
1411
|
except json.JSONDecodeError as e:
|
|
1306
1412
|
logger.debug(f"WebSocket received invalid JSON: {e}")
|
|
1307
1413
|
except asyncio.TimeoutError:
|
|
1308
|
-
|
|
1309
|
-
|
|
1414
|
+
missed_pongs += 1
|
|
1415
|
+
if missed_pongs >= 2:
|
|
1416
|
+
# Two consecutive pings with no reply -- close idle connection
|
|
1417
|
+
logger.info("Closing idle WebSocket (no pong response)")
|
|
1418
|
+
break
|
|
1419
|
+
try:
|
|
1420
|
+
await manager.send_personal(websocket, {"type": "ping"})
|
|
1421
|
+
except Exception:
|
|
1422
|
+
break
|
|
1310
1423
|
|
|
1311
1424
|
except WebSocketDisconnect:
|
|
1312
1425
|
manager.disconnect(websocket)
|
|
@@ -1437,7 +1550,13 @@ async def sync_registry():
|
|
|
1437
1550
|
if not _read_limiter.check("registry_sync"):
|
|
1438
1551
|
raise HTTPException(status_code=429, detail="Rate limit exceeded")
|
|
1439
1552
|
|
|
1440
|
-
|
|
1553
|
+
try:
|
|
1554
|
+
result = await asyncio.wait_for(
|
|
1555
|
+
asyncio.get_event_loop().run_in_executor(None, registry.sync_registry_with_discovery),
|
|
1556
|
+
timeout=30.0,
|
|
1557
|
+
)
|
|
1558
|
+
except asyncio.TimeoutError:
|
|
1559
|
+
raise HTTPException(status_code=504, detail="Registry sync timed out after 30 seconds")
|
|
1441
1560
|
return {
|
|
1442
1561
|
"added": result["added"],
|
|
1443
1562
|
"updated": result["updated"],
|
|
@@ -1608,7 +1727,7 @@ class AuditQueryParams(BaseModel):
|
|
|
1608
1727
|
offset: int = 0
|
|
1609
1728
|
|
|
1610
1729
|
|
|
1611
|
-
@app.get("/api/enterprise/audit")
|
|
1730
|
+
@app.get("/api/enterprise/audit", dependencies=[Depends(auth.require_scope("audit"))])
|
|
1612
1731
|
async def query_audit_logs(
|
|
1613
1732
|
start_date: Optional[str] = None,
|
|
1614
1733
|
end_date: Optional[str] = None,
|
|
@@ -1640,7 +1759,7 @@ async def query_audit_logs(
|
|
|
1640
1759
|
)
|
|
1641
1760
|
|
|
1642
1761
|
|
|
1643
|
-
@app.get("/api/enterprise/audit/summary")
|
|
1762
|
+
@app.get("/api/enterprise/audit/summary", dependencies=[Depends(auth.require_scope("audit"))])
|
|
1644
1763
|
async def get_audit_summary(days: int = 7):
|
|
1645
1764
|
"""Get audit activity summary."""
|
|
1646
1765
|
if not audit.is_audit_enabled():
|
|
@@ -1805,11 +1924,14 @@ async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
|
|
|
1805
1924
|
except Exception:
|
|
1806
1925
|
pass
|
|
1807
1926
|
|
|
1808
|
-
# Fallback to JSON files
|
|
1927
|
+
# Fallback to JSON files -- use heapq to avoid sorting all files
|
|
1928
|
+
import heapq
|
|
1809
1929
|
ep_dir = _get_loki_dir() / "memory" / "episodic"
|
|
1810
1930
|
episodes = []
|
|
1811
1931
|
if ep_dir.exists():
|
|
1812
|
-
|
|
1932
|
+
all_files = ep_dir.glob("*.json")
|
|
1933
|
+
# nlargest by filename (timestamps sort lexicographically) avoids full sort
|
|
1934
|
+
files = heapq.nlargest(limit, all_files, key=lambda f: f.name)
|
|
1813
1935
|
for f in files:
|
|
1814
1936
|
try:
|
|
1815
1937
|
episodes.append(json.loads(f.read_text()))
|
|
@@ -3515,7 +3637,7 @@ async def get_logs(lines: int = 100, token: Optional[dict] = Depends(auth.get_cu
|
|
|
3515
3637
|
file_mtime = datetime.fromtimestamp(log_file.stat().st_mtime, tz=timezone.utc).strftime(
|
|
3516
3638
|
"%Y-%m-%dT%H:%M:%S"
|
|
3517
3639
|
)
|
|
3518
|
-
content = log_file
|
|
3640
|
+
content = _safe_read_text(log_file)
|
|
3519
3641
|
for raw_line in content.strip().split("\n")[-lines:]:
|
|
3520
3642
|
timestamp = ""
|
|
3521
3643
|
level = "info"
|
|
@@ -4300,7 +4422,7 @@ async def get_app_runner_logs(lines: int = Query(default=100, ge=1, le=1000)):
|
|
|
4300
4422
|
if not log_file.exists():
|
|
4301
4423
|
return {"lines": []}
|
|
4302
4424
|
try:
|
|
4303
|
-
all_lines = log_file
|
|
4425
|
+
all_lines = _safe_read_text(log_file).splitlines()
|
|
4304
4426
|
return {"lines": all_lines[-lines:]}
|
|
4305
4427
|
except OSError:
|
|
4306
4428
|
return {"lines": []}
|
|
@@ -4563,25 +4685,16 @@ async def serve_index():
|
|
|
4563
4685
|
if os.path.isfile(index_path):
|
|
4564
4686
|
return FileResponse(index_path, media_type="text/html")
|
|
4565
4687
|
|
|
4566
|
-
# Return
|
|
4567
|
-
return
|
|
4568
|
-
content=
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
<p><strong>API Endpoints:</strong></p>
|
|
4577
|
-
<ul>
|
|
4578
|
-
<li><a href="/health">/health</a> - Health check</li>
|
|
4579
|
-
<li><a href="/docs">/docs</a> - API documentation</li>
|
|
4580
|
-
</ul>
|
|
4581
|
-
</body>
|
|
4582
|
-
</html>
|
|
4583
|
-
""",
|
|
4584
|
-
status_code=200
|
|
4688
|
+
# Return 503 when frontend files are not found
|
|
4689
|
+
return JSONResponse(
|
|
4690
|
+
content={
|
|
4691
|
+
"error": "dashboard_frontend_not_found",
|
|
4692
|
+
"detail": "The dashboard API is running, but the frontend files were not found. "
|
|
4693
|
+
"Run: cd dashboard-ui && npm run build",
|
|
4694
|
+
"api_docs": "/docs",
|
|
4695
|
+
"health": "/health",
|
|
4696
|
+
},
|
|
4697
|
+
status_code=503,
|
|
4585
4698
|
)
|
|
4586
4699
|
|
|
4587
4700
|
|
|
@@ -1237,7 +1237,7 @@ var LokiDashboard=(()=>{var pt=Object.defineProperty;var Pt=Object.getOwnPropert
|
|
|
1237
1237
|
}
|
|
1238
1238
|
|
|
1239
1239
|
${B}
|
|
1240
|
-
`}getAriaPattern(t){return gt[t]||{}}applyAriaPattern(t,e){let i=this.getAriaPattern(e);for(let[a,s]of Object.entries(i))if(a==="role")t.setAttribute("role",s);else{let r=a.replace(/([A-Z])/g,"-$1").toLowerCase();t.setAttribute(r,s)}}render(){}};var L={realtime:1e3,normal:2e3,background:5e3,offline:1e4},_t={vscode:L.normal,browser:L.realtime,cli:L.background},yt={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},u={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",CHECKLIST_UPDATE:"api:checklist-update"},C=class C extends EventTarget{static getInstance(t={}){let e=t.baseUrl||yt.baseUrl;return C._instances.has(e)||C._instances.set(e,new C(t)),C._instances.get(e)}static clearInstances(){C._instances.forEach(t=>t.disconnect()),C._instances.clear()}constructor(t={}){super(),this.config={...yt,...t},this._ws=null,this._connected=!1,this._pollInterval=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._maxReconnectAttempts=20,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=_t[this._context]||L.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 L}_setupAdaptivePolling(){typeof document>"u"||(this._visibilityChangeHandler=()=>{document.hidden?this._setPollInterval(L.background):this._setPollInterval(_t[this._context]||L.normal)},document.addEventListener("visibilitychange",this._visibilityChangeHandler))}_setPollInterval(t){this._currentPollInterval=t,this._pollInterval&&(this.stopPolling(),this.startPolling(null,t))}setPollMode(t){let e=L[t];e&&this._setPollInterval(e)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}this._messageHandler=t=>{let e=t.data;if(!(!e||!e.type))switch(e.type){case"updateStatus":this._emit(u.STATUS_UPDATE,e.data);break;case"updateTasks":this._emit(u.TASK_UPDATED,e.data);break;case"taskCreated":this._emit(u.TASK_CREATED,e.data);break;case"taskDeleted":this._emit(u.TASK_DELETED,e.data);break;case"projectCreated":this._emit(u.PROJECT_CREATED,e.data);break;case"projectUpdated":this._emit(u.PROJECT_UPDATED,e.data);break;case"agentUpdate":this._emit(u.AGENT_UPDATE,e.data);break;case"logMessage":this._emit(u.LOG_MESSAGE,e.data);break;case"memoryUpdate":this._emit(u.MEMORY_UPDATE,e.data);break;case"connected":this._connected=!0,this._emit(u.CONNECTED,e.data);break;case"disconnected":this._connected=!1,this._emit(u.DISCONNECTED,e.data);break;case"error":this._emit(u.ERROR,e.data);break;case"setPollMode":this.setPollMode(e.data.mode);break;default:this._emit(`api:${e.type}`,e.data)}},window.addEventListener("message",this._messageHandler)}}get isVSCode(){return this._context==="vscode"}postToVSCode(t,e={}){this._vscodeApi&&this._vscodeApi.postMessage({type:t,data:e})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(t,e={}){this.postToVSCode("userAction",{action:t,...e})}get baseUrl(){return this.config.baseUrl}set baseUrl(t){this.config.baseUrl=t,this.config.wsUrl=t.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((t,e)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._reconnectAttempts=0,this._emit(u.CONNECTED),t()},this._ws.onclose=()=>{this._connected=!1,this._emit(u.DISCONNECTED),this._scheduleReconnect()},this._ws.onerror=i=>{this._emit(u.ERROR,{error:i}),e(i)},this._ws.onmessage=i=>{try{let a=JSON.parse(i.data);this._handleMessage(a)}catch(a){console.error("Failed to parse WebSocket message:",a)}}}catch(i){e(i)}})}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(){if(this._reconnectTimeout)return;if(this._reconnectAttempts>=this._maxReconnectAttempts){console.warn("WebSocket max reconnect attempts reached, giving up"),this._emit(u.ERROR,{error:"Max reconnect attempts reached"});return}let t=Math.min(this.config.retryDelay*Math.pow(2,this._reconnectAttempts),3e4);this._reconnectAttempts++,this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},t)}_handleMessage(t){let i={connected:u.CONNECTED,status_update:u.STATUS_UPDATE,task_created:u.TASK_CREATED,task_updated:u.TASK_UPDATED,task_deleted:u.TASK_DELETED,task_moved:u.TASK_UPDATED,project_created:u.PROJECT_CREATED,project_updated:u.PROJECT_UPDATED,agent_update:u.AGENT_UPDATE,log:u.LOG_MESSAGE}[t.type]||`api:${t.type}`;this._emit(i,t.data)}_emit(t,e={}){this.dispatchEvent(new CustomEvent(t,{detail:e}))}async _request(t,e={}){let i=`${this.config.baseUrl}${t}`,a=new AbortController,s=setTimeout(()=>a.abort(),this.config.timeout);try{let r=await fetch(i,{...e,signal:a.signal,headers:{"Content-Type":"application/json",...e.headers}});if(clearTimeout(s),!r.ok){let o=await r.text().catch(()=>""),n=r.statusText||`HTTP ${r.status}`;if(o)try{let l=JSON.parse(o);n=l.detail||l.error||l.message||n}catch{n=o.length>200?o.slice(0,200)+"...":o}throw new Error(n)}return r.status===204?null:await r.json()}catch(r){throw clearTimeout(s),r.name==="AbortError"?new Error("Request timeout"):r}}async _get(t,e=!1){if(e&&this._cache.has(t)){let a=this._cache.get(t);if(Date.now()-a.timestamp<this._cacheTimeout)return a.data}let i=await this._request(t);return e&&this._cache.set(t,{data:i,timestamp:Date.now()}),i}async _post(t,e){return this._request(t,{method:"POST",body:JSON.stringify(e)})}async _put(t,e){return this._request(t,{method:"PUT",body:JSON.stringify(e)})}async _delete(t){return this._request(t,{method:"DELETE"})}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(t=null){let e=t?`?status=${t}`:"";return this._get(`/api/projects${e}`)}async getProject(t){return this._get(`/api/projects/${t}`)}async createProject(t){return this._post("/api/projects",t)}async updateProject(t,e){return this._put(`/api/projects/${t}`,e)}async deleteProject(t){return this._delete(`/api/projects/${t}`)}async listTasks(t={}){let e=new URLSearchParams;t.projectId&&e.append("project_id",t.projectId),t.status&&e.append("status",t.status),t.priority&&e.append("priority",t.priority);let i=e.toString()?`?${e}`:"";return this._get(`/api/tasks${i}`)}async getTask(t){return this._get(`/api/tasks/${t}`)}async createTask(t){return this._post("/api/tasks",t)}async updateTask(t,e){return this._put(`/api/tasks/${t}`,e)}async moveTask(t,e,i){return this._post(`/api/tasks/${t}/move`,{status:e,position:i})}async deleteTask(t){return this._delete(`/api/tasks/${t}`)}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(t={}){let e=new URLSearchParams(t).toString();return this._get(`/api/memory/episodes${e?"?"+e:""}`)}async getEpisode(t){return this._get(`/api/memory/episodes/${t}`)}async listPatterns(t={}){let e=new URLSearchParams(t).toString();return this._get(`/api/memory/patterns${e?"?"+e:""}`)}async getPattern(t){return this._get(`/api/memory/patterns/${t}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(t){return this._get(`/api/memory/skills/${t}`)}async retrieveMemories(t,e=null,i=5){return this._post("/api/memory/retrieve",{query:t,taskType:e,topK:i})}async consolidateMemory(t=24){return this._post("/api/memory/consolidate",{sinceHours:t})}async getTokenEconomics(){return this._get("/api/memory/economics")}async searchMemory(t,e="all",i=20){let a=new URLSearchParams({q:t,collection:e,limit:String(i)});return this._get(`/api/memory/search?${a}`)}async getMemoryStats(){return this._get("/api/memory/stats",!0)}async listRegisteredProjects(t=!1){return this._get(`/api/registry/projects?include_inactive=${t}`)}async registerProject(t,e=null,i=null){return this._post("/api/registry/projects",{path:t,name:e,alias:i})}async discoverProjects(t=3){return this._get(`/api/registry/discover?max_depth=${t}`)}async syncRegistry(){return this._post("/api/registry/sync",{})}async getCrossProjectTasks(t=null){let e=t?`?project_ids=${t.join(",")}`:"";return this._get(`/api/registry/tasks${e}`)}async getLearningMetrics(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source);let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/metrics${i}`)}async getLearningTrends(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source);let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/trends${i}`)}async getLearningSignals(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source),t.limit&&e.append("limit",String(t.limit)),t.offset&&e.append("offset",String(t.offset));let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/signals${i}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(t={}){return this._post("/api/learning/aggregate",t)}async getAggregatedPreferences(t=20){return this._get(`/api/learning/preferences?limit=${t}`)}async getAggregatedErrors(t=20){return this._get(`/api/learning/errors?limit=${t}`)}async getAggregatedSuccessPatterns(t=20){return this._get(`/api/learning/success?limit=${t}`)}async getToolEfficiency(t=20){return this._get(`/api/learning/tools?limit=${t}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getCouncilState(){return this._get("/api/council/state")}async getCouncilVerdicts(t=20){return this._get(`/api/council/verdicts?limit=${t}`)}async getCouncilConvergence(){return this._get("/api/council/convergence")}async getCouncilReport(){return this._get("/api/council/report")}async forceCouncilReview(){return this._post("/api/council/force-review",{})}async getContext(){return this._get("/api/context")}async getNotifications(t,e){let i=new URLSearchParams;t&&i.set("severity",t),e&&i.set("unread_only","true");let a=i.toString();return this._get("/api/notifications"+(a?"?"+a:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(t){return this._put("/api/notifications/triggers",{triggers:t})}async acknowledgeNotification(t){return this._post("/api/notifications/"+encodeURIComponent(t)+"/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(t=100){return this._get(`/api/logs?lines=${t}`)}async getChecklist(){return this._get("/api/checklist")}async getChecklistSummary(){return this._get("/api/checklist/summary")}async getPrdObservations(){let t=await fetch(`${this.baseUrl}/api/prd-observations`);if(!t.ok)throw new Error(`HTTP ${t.status}`);return t.text()}async getChecklistWaivers(){return this._get("/api/checklist/waivers")}async addChecklistWaiver(t,e,i="dashboard"){return this._post("/api/checklist/waivers",{item_id:t,reason:e,waived_by:i})}async removeChecklistWaiver(t){return this._delete(`/api/checklist/waivers/${encodeURIComponent(t)}`)}async getCouncilGate(){return this._get("/api/council/gate")}async getAppRunnerStatus(){return this._get("/api/app-runner/status")}async getAppRunnerLogs(t=100){return this._get(`/api/app-runner/logs?lines=${t}`)}async restartApp(){return this._post("/api/control/app-restart",{})}async stopApp(){return this._post("/api/control/app-stop",{})}async getPlaywrightResults(){return this._get("/api/playwright/results")}async getPlaywrightScreenshot(){return this._get("/api/playwright/screenshot")}startPolling(t,e=null){if(this._pollInterval)return;this._pollCallback=t;let i=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(u.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(u.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};i();let a=e||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(i,a)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};w(C,"_instances",new Map);var R=C;function wt(d={}){return new R(d)}function g(d={}){return R.getInstance(d)}var bt="loki-state-change",mt={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}},$=class $ extends EventTarget{static getInstance(){return $._instance||($._instance=new $),$._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let t=localStorage.getItem($.STORAGE_KEY);if(t){let e=JSON.parse(t);return this._mergeState(mt,e)}}catch(t){console.warn("Failed to load state from localStorage:",t)}return{...mt}}_mergeState(t,e){let i={...t};for(let a of Object.keys(e))a in t&&typeof t[a]=="object"&&!Array.isArray(t[a])?i[a]=this._mergeState(t[a],e[a]):i[a]=e[a];return i}_saveState(){try{let t={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem($.STORAGE_KEY,JSON.stringify(t))}catch(t){console.warn("Failed to save state to localStorage:",t)}}get(t=null){if(!t)return{...this._state};let e=t.split("."),i=this._state;for(let a of e){if(i==null)return;i=i[a]}return i}set(t,e,i=!0){let a=t.split("."),s=a.pop(),r=this._state;for(let n of a)n in r||(r[n]={}),r=r[n];let o=r[s];r[s]=e,i&&this._saveState(),this._notifyChange(t,e,o)}update(t,e=!0){let i=[];for(let[a,s]of Object.entries(t)){let r=this.get(a);this.set(a,s,!1),i.push({path:a,value:s,oldValue:r})}e&&this._saveState();for(let a of i)this._notifyChange(a.path,a.value,a.oldValue)}_notifyChange(t,e,i){this.dispatchEvent(new CustomEvent(bt,{detail:{path:t,value:e,oldValue:i}}));let a=this._subscribers.get(t)||[];for(let r of a)try{r(e,i,t)}catch(o){console.error("State subscriber error:",o)}let s=t.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let n of o)try{n(this.get(r),null,r)}catch(l){console.error("State subscriber error:",l)}}}subscribe(t,e){return this._subscribers.has(t)||this._subscribers.set(t,[]),this._subscribers.get(t).push(e),()=>{let i=this._subscribers.get(t),a=i.indexOf(e);a>-1&&i.splice(a,1)}}reset(t=null){if(t){let e=t.split("."),i=mt;for(let a of e)i=i?.[a];this.set(t,i)}else this._state={...mt},this._saveState(),this.dispatchEvent(new CustomEvent(bt,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(t){let e=this.get("localTasks")||[],i={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...t};return this.set("localTasks",[...e,i]),i}updateLocalTask(t,e){let i=this.get("localTasks")||[],a=i.findIndex(r=>r.id===t);if(a===-1)return null;let s={...i[a],...e,updatedAt:new Date().toISOString()};return i[a]=s,this.set("localTasks",[...i]),s}deleteLocalTask(t){let e=this.get("localTasks")||[];this.set("localTasks",e.filter(i=>i.id!==t))}moveLocalTask(t,e,i=null){let s=(this.get("localTasks")||[]).find(r=>r.id===t);return s?this.updateLocalTask(t,{status:e,position:i??s.position}):null}updateSession(t){this.update(Object.fromEntries(Object.entries(t).map(([e,i])=>[`session.${e}`,i])),!1)}updateCache(t){this.update({"cache.projects":t.projects??this.get("cache.projects"),"cache.tasks":t.tasks??this.get("cache.tasks"),"cache.agents":t.agents??this.get("cache.agents"),"cache.memory":t.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let t=this.get("cache.tasks")||[],i=(this.get("localTasks")||[]).map(a=>({...a,isLocal:!0}));return[...t,...i]}getTasksByStatus(t){return this.getMergedTasks().filter(e=>e.status===t)}};w($,"STORAGE_KEY","loki-dashboard-state"),w($,"_instance",null);var F=$;function z(){return F.getInstance()}function $t(d){let t=z();return{get:()=>t.get(d),set:e=>t.set(d,e),subscribe:e=>t.subscribe(d,e)}}var j=class extends h{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,this._checklistSummary=null,this._appRunnerStatus=null,this._playwrightResults=null,this._gateStatus=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling(),this._api.connect().catch(()=>{})}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._loadAbortController&&(this._loadAbortController.abort(),this._loadAbortController=null),this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(u.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(u.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadStatus()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._statusUpdateHandler=e=>this._updateFromStatus(e.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(u.CONNECTED,this._connectedHandler),this._api.addEventListener(u.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){this._loadAbortController&&this._loadAbortController.abort(),this._loadAbortController=new AbortController;let{signal:t}=this._loadAbortController;try{let[e,i,a,s,r]=await Promise.allSettled([this._api.getStatus(),this._api.getChecklistSummary(),this._api.getAppRunnerStatus(),this._api.getPlaywrightResults(),this._api.getCouncilGate()]);if(t.aborted)return;e.status==="fulfilled"?this._updateFromStatus(e.value):(this._data.connected=!1,this._data.status="offline"),i.status==="fulfilled"&&(this._checklistSummary=i.value?.summary||null),a.status==="fulfilled"&&(this._appRunnerStatus=a.value),s.status==="fulfilled"&&(this._playwrightResults=s.value),r.status==="fulfilled"&&(this._gateStatus=r.value),this.render()}catch{if(t.aborted)return;this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(t){t&&(this._data={...this._data,connected:!0,status:t.status||"offline",phase:t.phase||null,iteration:t.iteration!=null?t.iteration:null,provider:t.provider||null,running_agents:t.running_agents||0,pending_tasks:t.pending_tasks!=null?t.pending_tasks:null,uptime_seconds:t.uptime_seconds||0,complexity:t.complexity||null})}_startPolling(){this._pollInterval=setInterval(async()=>{try{await this._loadStatus()}catch{this._data.connected=!1,this._data.status="offline",this.render()}},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_formatUptime(t){if(!t||t<0)return"--";let e=Math.floor(t/3600),i=Math.floor(t%3600/60),a=Math.floor(t%60);return e>0?`${e}h ${i}m`:i>0?`${i}m ${a}s`:`${a}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"}}_escapeHtml(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_renderAppRunnerCard(){let t=this._appRunnerStatus;if(!t||t.status==="not_initialized")return`
|
|
1240
|
+
`}getAriaPattern(t){return gt[t]||{}}applyAriaPattern(t,e){let i=this.getAriaPattern(e);for(let[a,s]of Object.entries(i))if(a==="role")t.setAttribute("role",s);else{let r=a.replace(/([A-Z])/g,"-$1").toLowerCase();t.setAttribute(r,s)}}render(){}};var L={realtime:1e3,normal:2e3,background:5e3,offline:1e4},_t={vscode:L.normal,browser:L.realtime,cli:L.background},yt={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},u={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",CHECKLIST_UPDATE:"api:checklist-update"},C=class C extends EventTarget{static getInstance(t={}){let e=t.baseUrl||yt.baseUrl;return C._instances.has(e)||C._instances.set(e,new C(t)),C._instances.get(e)}static clearInstances(){C._instances.forEach(t=>t.disconnect()),C._instances.clear()}constructor(t={}){super(),this.config={...yt,...t},this._ws=null,this._connected=!1,this._pollInterval=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._maxReconnectAttempts=20,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=_t[this._context]||L.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 L}_setupAdaptivePolling(){typeof document>"u"||(this._visibilityChangeHandler=()=>{document.hidden?this._setPollInterval(L.background):this._setPollInterval(_t[this._context]||L.normal)},document.addEventListener("visibilitychange",this._visibilityChangeHandler))}_setPollInterval(t){this._currentPollInterval=t,this._pollInterval&&(this.stopPolling(),this.startPolling(null,t))}setPollMode(t){let e=L[t];e&&this._setPollInterval(e)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}this._messageHandler=t=>{let e=t.data;if(!(!e||!e.type))switch(e.type){case"updateStatus":this._emit(u.STATUS_UPDATE,e.data);break;case"updateTasks":this._emit(u.TASK_UPDATED,e.data);break;case"taskCreated":this._emit(u.TASK_CREATED,e.data);break;case"taskDeleted":this._emit(u.TASK_DELETED,e.data);break;case"projectCreated":this._emit(u.PROJECT_CREATED,e.data);break;case"projectUpdated":this._emit(u.PROJECT_UPDATED,e.data);break;case"agentUpdate":this._emit(u.AGENT_UPDATE,e.data);break;case"logMessage":this._emit(u.LOG_MESSAGE,e.data);break;case"memoryUpdate":this._emit(u.MEMORY_UPDATE,e.data);break;case"connected":this._connected=!0,this._emit(u.CONNECTED,e.data);break;case"disconnected":this._connected=!1,this._emit(u.DISCONNECTED,e.data);break;case"error":this._emit(u.ERROR,e.data);break;case"setPollMode":this.setPollMode(e.data.mode);break;default:this._emit(`api:${e.type}`,e.data)}},window.addEventListener("message",this._messageHandler)}}get isVSCode(){return this._context==="vscode"}postToVSCode(t,e={}){this._vscodeApi&&this._vscodeApi.postMessage({type:t,data:e})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(t,e={}){this.postToVSCode("userAction",{action:t,...e})}get baseUrl(){return this.config.baseUrl}set baseUrl(t){this.config.baseUrl=t,this.config.wsUrl=t.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((t,e)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._reconnectAttempts=0,this._emit(u.CONNECTED),t()},this._ws.onclose=()=>{this._connected=!1,this._emit(u.DISCONNECTED),this._scheduleReconnect()},this._ws.onerror=i=>{this._emit(u.ERROR,{error:i}),e(i)},this._ws.onmessage=i=>{try{let a=JSON.parse(i.data);this._handleMessage(a)}catch(a){console.error("Failed to parse WebSocket message:",a)}}}catch(i){e(i)}})}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(){if(this._reconnectTimeout)return;if(this._reconnectAttempts>=this._maxReconnectAttempts){console.warn("WebSocket max reconnect attempts reached, giving up"),this._emit(u.ERROR,{error:"Max reconnect attempts reached"});return}let t=Math.min(this.config.retryDelay*Math.pow(2,this._reconnectAttempts),3e4);this._reconnectAttempts++,this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},t)}_handleMessage(t){if(t.type==="ping"){this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"pong"}));return}let i={connected:u.CONNECTED,status_update:u.STATUS_UPDATE,task_created:u.TASK_CREATED,task_updated:u.TASK_UPDATED,task_deleted:u.TASK_DELETED,task_moved:u.TASK_UPDATED,project_created:u.PROJECT_CREATED,project_updated:u.PROJECT_UPDATED,agent_update:u.AGENT_UPDATE,log:u.LOG_MESSAGE}[t.type]||`api:${t.type}`;this._emit(i,t.data)}_emit(t,e={}){this.dispatchEvent(new CustomEvent(t,{detail:e}))}async _request(t,e={}){let i=`${this.config.baseUrl}${t}`,a=new AbortController,s=setTimeout(()=>a.abort(),this.config.timeout);try{let r=await fetch(i,{...e,signal:a.signal,credentials:"include",headers:{"Content-Type":"application/json",...e.headers}});if(clearTimeout(s),!r.ok){let o=await r.text().catch(()=>""),n=r.statusText||`HTTP ${r.status}`;if(o)try{let l=JSON.parse(o);n=l.detail||l.error||l.message||n}catch{n=o.length>200?o.slice(0,200)+"...":o}throw new Error(n)}return r.status===204?null:await r.json()}catch(r){throw clearTimeout(s),r.name==="AbortError"?new Error("Request timeout"):r}}async _get(t,e=!1){if(e&&this._cache.has(t)){let a=this._cache.get(t);if(Date.now()-a.timestamp<this._cacheTimeout)return a.data}let i=await this._request(t);return e&&this._cache.set(t,{data:i,timestamp:Date.now()}),i}async _post(t,e){return this._request(t,{method:"POST",body:JSON.stringify(e)})}async _put(t,e){return this._request(t,{method:"PUT",body:JSON.stringify(e)})}async _delete(t){return this._request(t,{method:"DELETE"})}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(t=null){let e=t?`?status=${t}`:"";return this._get(`/api/projects${e}`)}async getProject(t){return this._get(`/api/projects/${t}`)}async createProject(t){return this._post("/api/projects",t)}async updateProject(t,e){return this._put(`/api/projects/${t}`,e)}async deleteProject(t){return this._delete(`/api/projects/${t}`)}async listTasks(t={}){let e=new URLSearchParams;t.projectId&&e.append("project_id",t.projectId),t.status&&e.append("status",t.status),t.priority&&e.append("priority",t.priority);let i=e.toString()?`?${e}`:"";return this._get(`/api/tasks${i}`)}async getTask(t){return this._get(`/api/tasks/${t}`)}async createTask(t){return this._post("/api/tasks",t)}async updateTask(t,e){return this._put(`/api/tasks/${t}`,e)}async moveTask(t,e,i){return this._post(`/api/tasks/${t}/move`,{status:e,position:i})}async deleteTask(t){return this._delete(`/api/tasks/${t}`)}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(t={}){let e=new URLSearchParams(t).toString();return this._get(`/api/memory/episodes${e?"?"+e:""}`)}async getEpisode(t){return this._get(`/api/memory/episodes/${t}`)}async listPatterns(t={}){let e=new URLSearchParams(t).toString();return this._get(`/api/memory/patterns${e?"?"+e:""}`)}async getPattern(t){return this._get(`/api/memory/patterns/${t}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(t){return this._get(`/api/memory/skills/${t}`)}async retrieveMemories(t,e=null,i=5){return this._post("/api/memory/retrieve",{query:t,taskType:e,topK:i})}async consolidateMemory(t=24){return this._post("/api/memory/consolidate",{sinceHours:t})}async getTokenEconomics(){return this._get("/api/memory/economics")}async searchMemory(t,e="all",i=20){let a=new URLSearchParams({q:t,collection:e,limit:String(i)});return this._get(`/api/memory/search?${a}`)}async getMemoryStats(){return this._get("/api/memory/stats",!0)}async listRegisteredProjects(t=!1){return this._get(`/api/registry/projects?include_inactive=${t}`)}async registerProject(t,e=null,i=null){return this._post("/api/registry/projects",{path:t,name:e,alias:i})}async discoverProjects(t=3){return this._get(`/api/registry/discover?max_depth=${t}`)}async syncRegistry(){return this._post("/api/registry/sync",{})}async getCrossProjectTasks(t=null){let e=t?`?project_ids=${t.join(",")}`:"";return this._get(`/api/registry/tasks${e}`)}async getLearningMetrics(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source);let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/metrics${i}`)}async getLearningTrends(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source);let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/trends${i}`)}async getLearningSignals(t={}){let e=new URLSearchParams;t.timeRange&&e.append("timeRange",t.timeRange),t.signalType&&e.append("signalType",t.signalType),t.source&&e.append("source",t.source),t.limit&&e.append("limit",String(t.limit)),t.offset&&e.append("offset",String(t.offset));let i=e.toString()?`?${e}`:"";return this._get(`/api/learning/signals${i}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(t={}){return this._post("/api/learning/aggregate",t)}async getAggregatedPreferences(t=20){return this._get(`/api/learning/preferences?limit=${t}`)}async getAggregatedErrors(t=20){return this._get(`/api/learning/errors?limit=${t}`)}async getAggregatedSuccessPatterns(t=20){return this._get(`/api/learning/success?limit=${t}`)}async getToolEfficiency(t=20){return this._get(`/api/learning/tools?limit=${t}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getCouncilState(){return this._get("/api/council/state")}async getCouncilVerdicts(t=20){return this._get(`/api/council/verdicts?limit=${t}`)}async getCouncilConvergence(){return this._get("/api/council/convergence")}async getCouncilReport(){return this._get("/api/council/report")}async forceCouncilReview(){return this._post("/api/council/force-review",{})}async getContext(){return this._get("/api/context")}async getNotifications(t,e){let i=new URLSearchParams;t&&i.set("severity",t),e&&i.set("unread_only","true");let a=i.toString();return this._get("/api/notifications"+(a?"?"+a:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(t){return this._put("/api/notifications/triggers",{triggers:t})}async acknowledgeNotification(t){return this._post("/api/notifications/"+encodeURIComponent(t)+"/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(t=100){return this._get(`/api/logs?lines=${t}`)}async getChecklist(){return this._get("/api/checklist")}async getChecklistSummary(){return this._get("/api/checklist/summary")}async getPrdObservations(){let t=await fetch(`${this.baseUrl}/api/prd-observations`,{credentials:"include"});if(!t.ok)throw new Error(`HTTP ${t.status}`);return t.text()}async getChecklistWaivers(){return this._get("/api/checklist/waivers")}async addChecklistWaiver(t,e,i="dashboard"){return this._post("/api/checklist/waivers",{item_id:t,reason:e,waived_by:i})}async removeChecklistWaiver(t){return this._delete(`/api/checklist/waivers/${encodeURIComponent(t)}`)}async getCouncilGate(){return this._get("/api/council/gate")}async getAppRunnerStatus(){return this._get("/api/app-runner/status")}async getAppRunnerLogs(t=100){return this._get(`/api/app-runner/logs?lines=${t}`)}async restartApp(){return this._post("/api/control/app-restart",{})}async stopApp(){return this._post("/api/control/app-stop",{})}async getPlaywrightResults(){return this._get("/api/playwright/results")}async getPlaywrightScreenshot(){return this._get("/api/playwright/screenshot")}startPolling(t,e=null){if(this._pollInterval)return;this._pollCallback=t;let i=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(u.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(u.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};i();let a=e||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(i,a)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};w(C,"_instances",new Map);var R=C;function wt(d={}){return new R(d)}function g(d={}){return R.getInstance(d)}var bt="loki-state-change",mt={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}},$=class $ extends EventTarget{static getInstance(){return $._instance||($._instance=new $),$._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let t=localStorage.getItem($.STORAGE_KEY);if(t){let e=JSON.parse(t);return this._mergeState(mt,e)}}catch(t){console.warn("Failed to load state from localStorage:",t)}return{...mt}}_mergeState(t,e){let i={...t};for(let a of Object.keys(e))a in t&&typeof t[a]=="object"&&!Array.isArray(t[a])?i[a]=this._mergeState(t[a],e[a]):i[a]=e[a];return i}_saveState(){try{let t={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem($.STORAGE_KEY,JSON.stringify(t))}catch(t){console.warn("Failed to save state to localStorage:",t)}}get(t=null){if(!t)return{...this._state};let e=t.split("."),i=this._state;for(let a of e){if(i==null)return;i=i[a]}return i}set(t,e,i=!0){let a=t.split("."),s=a.pop(),r=this._state;for(let n of a)n in r||(r[n]={}),r=r[n];let o=r[s];r[s]=e,i&&this._saveState(),this._notifyChange(t,e,o)}update(t,e=!0){let i=[];for(let[a,s]of Object.entries(t)){let r=this.get(a);this.set(a,s,!1),i.push({path:a,value:s,oldValue:r})}e&&this._saveState();for(let a of i)this._notifyChange(a.path,a.value,a.oldValue)}_notifyChange(t,e,i){this.dispatchEvent(new CustomEvent(bt,{detail:{path:t,value:e,oldValue:i}}));let a=this._subscribers.get(t)||[];for(let r of a)try{r(e,i,t)}catch(o){console.error("State subscriber error:",o)}let s=t.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let n of o)try{n(this.get(r),null,r)}catch(l){console.error("State subscriber error:",l)}}}subscribe(t,e){return this._subscribers.has(t)||this._subscribers.set(t,[]),this._subscribers.get(t).push(e),()=>{let i=this._subscribers.get(t),a=i.indexOf(e);a>-1&&i.splice(a,1)}}reset(t=null){if(t){let e=t.split("."),i=mt;for(let a of e)i=i?.[a];this.set(t,i)}else this._state={...mt},this._saveState(),this.dispatchEvent(new CustomEvent(bt,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(t){let e=this.get("localTasks")||[],i={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...t};return this.set("localTasks",[...e,i]),i}updateLocalTask(t,e){let i=this.get("localTasks")||[],a=i.findIndex(r=>r.id===t);if(a===-1)return null;let s={...i[a],...e,updatedAt:new Date().toISOString()};return i[a]=s,this.set("localTasks",[...i]),s}deleteLocalTask(t){let e=this.get("localTasks")||[];this.set("localTasks",e.filter(i=>i.id!==t))}moveLocalTask(t,e,i=null){let s=(this.get("localTasks")||[]).find(r=>r.id===t);return s?this.updateLocalTask(t,{status:e,position:i??s.position}):null}updateSession(t){this.update(Object.fromEntries(Object.entries(t).map(([e,i])=>[`session.${e}`,i])),!1)}updateCache(t){this.update({"cache.projects":t.projects??this.get("cache.projects"),"cache.tasks":t.tasks??this.get("cache.tasks"),"cache.agents":t.agents??this.get("cache.agents"),"cache.memory":t.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let t=this.get("cache.tasks")||[],i=(this.get("localTasks")||[]).map(a=>({...a,isLocal:!0}));return[...t,...i]}getTasksByStatus(t){return this.getMergedTasks().filter(e=>e.status===t)}};w($,"STORAGE_KEY","loki-dashboard-state"),w($,"_instance",null);var F=$;function z(){return F.getInstance()}function $t(d){let t=z();return{get:()=>t.get(d),set:e=>t.set(d,e),subscribe:e=>t.subscribe(d,e)}}var j=class extends h{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,this._checklistSummary=null,this._appRunnerStatus=null,this._playwrightResults=null,this._gateStatus=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling(),this._api.connect().catch(()=>{})}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._loadAbortController&&(this._loadAbortController.abort(),this._loadAbortController=null),this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(u.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(u.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadStatus()),t==="theme"&&this._applyTheme())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._statusUpdateHandler=e=>this._updateFromStatus(e.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(u.CONNECTED,this._connectedHandler),this._api.addEventListener(u.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){this._loadAbortController&&this._loadAbortController.abort(),this._loadAbortController=new AbortController;let{signal:t}=this._loadAbortController;try{let[e,i,a,s,r]=await Promise.allSettled([this._api.getStatus(),this._api.getChecklistSummary(),this._api.getAppRunnerStatus(),this._api.getPlaywrightResults(),this._api.getCouncilGate()]);if(t.aborted)return;e.status==="fulfilled"?this._updateFromStatus(e.value):(this._data.connected=!1,this._data.status="offline"),i.status==="fulfilled"&&(this._checklistSummary=i.value?.summary||null),a.status==="fulfilled"&&(this._appRunnerStatus=a.value),s.status==="fulfilled"&&(this._playwrightResults=s.value),r.status==="fulfilled"&&(this._gateStatus=r.value),this.render()}catch{if(t.aborted)return;this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(t){t&&(this._data={...this._data,connected:!0,status:t.status||"offline",phase:t.phase||null,iteration:t.iteration!=null?t.iteration:null,provider:t.provider||null,running_agents:t.running_agents||0,pending_tasks:t.pending_tasks!=null?t.pending_tasks:null,uptime_seconds:t.uptime_seconds||0,complexity:t.complexity||null})}_startPolling(){this._pollInterval=setInterval(async()=>{try{await this._loadStatus()}catch{this._data.connected=!1,this._data.status="offline",this.render()}},5e3)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}_formatUptime(t){if(!t||t<0)return"--";let e=Math.floor(t/3600),i=Math.floor(t%3600/60),a=Math.floor(t%60);return e>0?`${e}h ${i}m`:i>0?`${i}m ${a}s`:`${a}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"}}_escapeHtml(t){return t?String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):""}_renderAppRunnerCard(){let t=this._appRunnerStatus;if(!t||t.status==="not_initialized")return`
|
|
1241
1241
|
<div class="overview-card">
|
|
1242
1242
|
<div class="card-label">App Runner</div>
|
|
1243
1243
|
<div class="card-value small-text">${this._data.status==="running"||this._data.status==="autonomous"?"Waiting...":"Not started"}</div>
|
|
@@ -2028,7 +2028,7 @@ var LokiDashboard=(()=>{var pt=Object.defineProperty;var Pt=Object.getOwnPropert
|
|
|
2028
2028
|
${i}
|
|
2029
2029
|
</div>
|
|
2030
2030
|
${this._selectedTask?this._renderTaskDetailModal(this._selectedTask):""}
|
|
2031
|
-
`,this._attachEventListeners()}_attachEventListeners(){let t=this.shadowRoot.getElementById("refresh-btn");t&&t.addEventListener("click",()=>this._loadTasks()),this.shadowRoot.querySelectorAll(".add-task-btn").forEach(a=>{a.addEventListener("click",()=>{this._openAddTaskModal(a.dataset.status)})}),this.shadowRoot.querySelectorAll(".task-card").forEach(a=>{let s=a.dataset.taskId,r=this._tasks.find(o=>o.id.toString()===s);r&&(a.addEventListener("click",()=>this._openTaskDetail(r)),a.addEventListener("keydown",o=>{o.key==="Enter"||o.key===" "?(o.preventDefault(),this._openTaskDetail(r)):(o.key==="ArrowDown"||o.key==="ArrowUp")&&(o.preventDefault(),this._navigateTaskCards(a,o.key==="ArrowDown"?"next":"prev"))}),a.classList.contains("draggable")&&(a.addEventListener("dragstart",o=>this._handleDragStart(o,r)),a.addEventListener("dragend",o=>this._handleDragEnd(o))))}),this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(a=>{a.addEventListener("dragover",s=>this._handleDragOver(s)),a.addEventListener("dragenter",s=>this._handleDragEnter(s)),a.addEventListener("dragleave",s=>this._handleDragLeave(s)),a.addEventListener("drop",s=>this._handleDrop(s,a.dataset.status))});let e=this.shadowRoot.getElementById("modal-close-btn");e&&e.addEventListener("click",()=>this._closeTaskDetail());let i=this.shadowRoot.getElementById("task-detail-overlay");i&&i.addEventListener("click",a=>{a.target===i&&this._closeTaskDetail()})}_escapeHtml(t){let e=document.createElement("div");return e.textContent=t,e.innerHTML}_navigateTaskCards(t,e){let i=Array.from(this.shadowRoot.querySelectorAll(".task-card")),a=i.indexOf(t);if(a===-1)return;let s=e==="next"?a+1:a-1;s>=0&&s<i.length&&i[s].focus()}};customElements.get("loki-task-board")||customElements.define("loki-task-board",U);var O=class extends h{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=z(),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(u.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(u.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(u.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadStatus()),t==="theme"&&this._applyTheme(),t==="compact"&&this.render())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._statusUpdateHandler=e=>this._updateFromStatus(e.detail),this._connectedHandler=()=>{this._status.connected=!0,this.render()},this._disconnectedHandler=()=>{this._status.connected=!1,this._status.mode="offline",this.render()},this._api.addEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(u.CONNECTED,this._connectedHandler),this._api.addEventListener(u.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){try{let t=await this._api.getStatus();this._updateFromStatus(t)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}}_updateFromStatus(t){t&&(this._status={...this._status,connected:!0,mode:t.status||"running",version:t.version,uptime:t.uptime_seconds||0,activeAgents:t.running_agents||0,pendingTasks:t.pending_tasks||0,phase:t.phase,iteration:t.iteration,complexity:t.complexity},this._state.updateSession({connected:!0,mode:this._status.mode,lastSync:new Date().toISOString()}),this.render())}_startPolling(){this._ownPollInterval=setInterval(async()=>{try{let t=await this._api.getStatus();this._updateFromStatus(t)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}},3e3)}_stopPolling(){this._ownPollInterval&&(clearInterval(this._ownPollInterval),this._ownPollInterval=null)}_formatUptime(t){if(!t||t<0)return"--";let e=Math.floor(t/3600),i=Math.floor(t%3600/60),a=Math.floor(t%60);return e>0?`${e}h ${i}m`:i>0?`${i}m ${a}s`:`${a}s`}_escapeHtml(t){let e=document.createElement("div");return e.textContent=String(t??""),e.innerHTML}_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()
|
|
2031
|
+
`,this._attachEventListeners()}_attachEventListeners(){let t=this.shadowRoot.getElementById("refresh-btn");t&&t.addEventListener("click",()=>this._loadTasks()),this.shadowRoot.querySelectorAll(".add-task-btn").forEach(a=>{a.addEventListener("click",()=>{this._openAddTaskModal(a.dataset.status)})}),this.shadowRoot.querySelectorAll(".task-card").forEach(a=>{let s=a.dataset.taskId,r=this._tasks.find(o=>o.id.toString()===s);r&&(a.addEventListener("click",()=>this._openTaskDetail(r)),a.addEventListener("keydown",o=>{o.key==="Enter"||o.key===" "?(o.preventDefault(),this._openTaskDetail(r)):(o.key==="ArrowDown"||o.key==="ArrowUp")&&(o.preventDefault(),this._navigateTaskCards(a,o.key==="ArrowDown"?"next":"prev"))}),a.classList.contains("draggable")&&(a.addEventListener("dragstart",o=>this._handleDragStart(o,r)),a.addEventListener("dragend",o=>this._handleDragEnd(o))))}),this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(a=>{a.addEventListener("dragover",s=>this._handleDragOver(s)),a.addEventListener("dragenter",s=>this._handleDragEnter(s)),a.addEventListener("dragleave",s=>this._handleDragLeave(s)),a.addEventListener("drop",s=>this._handleDrop(s,a.dataset.status))});let e=this.shadowRoot.getElementById("modal-close-btn");e&&e.addEventListener("click",()=>this._closeTaskDetail());let i=this.shadowRoot.getElementById("task-detail-overlay");i&&i.addEventListener("click",a=>{a.target===i&&this._closeTaskDetail()})}_escapeHtml(t){let e=document.createElement("div");return e.textContent=t,e.innerHTML}_navigateTaskCards(t,e){let i=Array.from(this.shadowRoot.querySelectorAll(".task-card")),a=i.indexOf(t);if(a===-1)return;let s=e==="next"?a+1:a-1;s>=0&&s<i.length&&i[s].focus()}};customElements.get("loki-task-board")||customElements.define("loki-task-board",U);var O=class extends h{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=z(),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(u.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(u.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(u.DISCONNECTED,this._disconnectedHandler))}attributeChangedCallback(t,e,i){e!==i&&(t==="api-url"&&this._api&&(this._api.baseUrl=i,this._loadStatus()),t==="theme"&&this._applyTheme(),t==="compact"&&this.render())}_setupApi(){let t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._statusUpdateHandler=e=>this._updateFromStatus(e.detail),this._connectedHandler=()=>{this._status.connected=!0,this.render()},this._disconnectedHandler=()=>{this._status.connected=!1,this._status.mode="offline",this.render()},this._api.addEventListener(u.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(u.CONNECTED,this._connectedHandler),this._api.addEventListener(u.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){try{let t=await this._api.getStatus();this._updateFromStatus(t)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}}_updateFromStatus(t){t&&(this._status={...this._status,connected:!0,mode:t.status||"running",version:t.version,uptime:t.uptime_seconds||0,activeAgents:t.running_agents||0,pendingTasks:t.pending_tasks||0,phase:t.phase,iteration:t.iteration,complexity:t.complexity},this._state.updateSession({connected:!0,mode:this._status.mode,lastSync:new Date().toISOString()}),this.render())}_startPolling(){this._ownPollInterval=setInterval(async()=>{try{let t=await this._api.getStatus();this._updateFromStatus(t)}catch{this._status.connected=!1,this._status.mode="offline",this.render()}},3e3)}_stopPolling(){this._ownPollInterval&&(clearInterval(this._ownPollInterval),this._ownPollInterval=null)}_formatUptime(t){if(!t||t<0)return"--";let e=Math.floor(t/3600),i=Math.floor(t%3600/60),a=Math.floor(t%60);return e>0?`${e}h ${i}m`:i>0?`${i}m ${a}s`:`${a}s`}_escapeHtml(t){let e=document.createElement("div");return e.textContent=String(t??""),e.innerHTML}_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{let t=await this._api.pauseSession();if(t&&t.error)throw new Error(t.error);this._status.mode="paused",this.render(),this.dispatchEvent(new CustomEvent("session-pause",{detail:this._status}))}catch(t){console.error("Failed to pause session:",t),this.render()}}async _triggerResume(){try{let t=await this._api.resumeSession();if(t&&t.error)throw new Error(t.error);this._status.mode="running",this.render(),this.dispatchEvent(new CustomEvent("session-resume",{detail:this._status}))}catch(t){console.error("Failed to resume session:",t),this.render()}}async _triggerStop(){try{let t=await this._api.stopSession();if(t&&t.error)throw new Error(t.error);this._status.mode="stopped",this.render(),this.dispatchEvent(new CustomEvent("session-stop",{detail:this._status}))}catch(t){console.error("Failed to stop session:",t),this.render()}}render(){let t=this.hasAttribute("compact"),e=this._getStatusClass(),i=this._getStatusLabel(),a=["running","autonomous"].includes(this._status.mode),s=this._status.mode==="paused",r=`
|
|
2032
2032
|
<style>
|
|
2033
2033
|
${this.getBaseStyles()}
|
|
2034
2034
|
|
|
@@ -2307,7 +2307,7 @@ var LokiDashboard=(()=>{var pt=Object.defineProperty;var Pt=Object.getOwnPropert
|
|
|
2307
2307
|
`;this.shadowRoot.innerHTML=`
|
|
2308
2308
|
${r}
|
|
2309
2309
|
${t?o:n}
|
|
2310
|
-
`,this._attachEventListeners()}_attachEventListeners(){let t=this.shadowRoot.getElementById("pause-btn"),e=this.shadowRoot.getElementById("resume-btn"),i=this.shadowRoot.getElementById("stop-btn"),a=this.shadowRoot.getElementById("start-btn");t&&t.addEventListener("click",()=>this._triggerPause()),e&&e.addEventListener("click",()=>this._triggerResume()),i&&i.addEventListener("click",()=>this._triggerStop()),a&&a.addEventListener("click",()=>this._triggerStart())}};customElements.get("loki-session-control")||customElements.define("loki-session-control",O);var Et={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"}},N=class extends h{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(u.LOG_MESSAGE,this._logMessageHandler)}attributeChangedCallback(t,e,i){if(e!==i)switch(t){case"api-url":this._api&&(this._api.baseUrl=i);break;case"max-lines":this._maxLines=parseInt(i)||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 t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._logMessageHandler=e=>this._addLog(e.detail),this._api.addEventListener(u.LOG_MESSAGE,this._logMessageHandler)}_startLogPolling(){let t=this.getAttribute("log-file");t?this._pollLogFile(t):this._pollApiLogs()}async _pollApiLogs(){let t=0,e=async()=>{try{let i=await this._api.getLogs(200);if(Array.isArray(i)&&i.length>t){let a=i.slice(t);for(let s of a)s.message&&s.message.trim()&&this._addLog({message:s.message,level:s.level||"info",timestamp:s.timestamp||new Date().toLocaleTimeString()});t=i.length}}catch{}};e(),this._apiPollInterval=setInterval(e,2e3)}async _pollLogFile(t){let e=0,i=async()=>{try{let a=await fetch(`${t}?t=${Date.now()}
|
|
2310
|
+
`,this._attachEventListeners()}_attachEventListeners(){let t=this.shadowRoot.getElementById("pause-btn"),e=this.shadowRoot.getElementById("resume-btn"),i=this.shadowRoot.getElementById("stop-btn"),a=this.shadowRoot.getElementById("start-btn");t&&t.addEventListener("click",()=>this._triggerPause()),e&&e.addEventListener("click",()=>this._triggerResume()),i&&i.addEventListener("click",()=>this._triggerStop()),a&&a.addEventListener("click",()=>this._triggerStart())}};customElements.get("loki-session-control")||customElements.define("loki-session-control",O);var Et={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"}},N=class extends h{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(u.LOG_MESSAGE,this._logMessageHandler)}attributeChangedCallback(t,e,i){if(e!==i)switch(t){case"api-url":this._api&&(this._api.baseUrl=i);break;case"max-lines":this._maxLines=parseInt(i)||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 t=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:t}),this._logMessageHandler=e=>this._addLog(e.detail),this._api.addEventListener(u.LOG_MESSAGE,this._logMessageHandler)}_startLogPolling(){let t=this.getAttribute("log-file");t?this._pollLogFile(t):this._pollApiLogs()}async _pollApiLogs(){let t=0,e=async()=>{try{let i=await this._api.getLogs(200);if(Array.isArray(i)&&i.length>t){let a=i.slice(t);for(let s of a)s.message&&s.message.trim()&&this._addLog({message:s.message,level:s.level||"info",timestamp:s.timestamp||new Date().toLocaleTimeString()});t=i.length}}catch{}};e(),this._apiPollInterval=setInterval(e,2e3)}async _pollLogFile(t){let e=0,i=async()=>{try{let a=await fetch(`${t}?t=${Date.now()}`,{credentials:"include"});if(!a.ok)return;let r=(await a.text()).split(`
|
|
2311
2311
|
`);if(r.length>e){let o=r.slice(e);for(let n of o)n.trim()&&this._addLog(this._parseLine(n));e=r.length}}catch{}};i(),this._pollInterval=setInterval(i,1e3)}_stopLogPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._apiPollInterval&&(clearInterval(this._apiPollInterval),this._apiPollInterval=null)}_parseLine(t){let e=t.match(/^\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.+)$/);if(e)return{timestamp:e[1],level:e[2].toLowerCase(),message:e[3]};let i=t.match(/^(\d{2}:\d{2}:\d{2})\s+(\w+)\s+(.+)$/);return i?{timestamp:i[1],level:i[2].toLowerCase(),message:i[3]}:{timestamp:new Date().toLocaleTimeString(),level:"info",message:t}}_addLog(t){if(!t)return;let e={id:Date.now()+Math.random(),timestamp:t.timestamp||new Date().toLocaleTimeString(),level:(t.level||"info").toLowerCase(),message:t.message||t};this._logs.push(e),this._trimLogs(),this.dispatchEvent(new CustomEvent("log-received",{detail:e})),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 t=this.shadowRoot.getElementById("log-output");t&&(t.scrollTop=t.scrollHeight)})}_downloadLogs(){let t=this._logs.map(s=>`[${s.timestamp}] [${s.level.toUpperCase()}] ${s.message}`).join(`
|
|
2312
2312
|
`),e=new Blob([t],{type:"text/plain"}),i=URL.createObjectURL(e),a=document.createElement("a");a.href=i,a.download=`loki-logs-${new Date().toISOString().split("T")[0]}.txt`,a.click(),URL.revokeObjectURL(i)}_setFilter(t){this._filter=t.toLowerCase(),this._renderLogs()}_setLevelFilter(t){this._levelFilter=t,this._renderLogs()}_getFilteredLogs(){return this._logs.filter(t=>!(this._levelFilter!=="all"&&t.level!==this._levelFilter||this._filter&&!t.message.toLowerCase().includes(this._filter)))}_renderLogs(){let t=this.shadowRoot.getElementById("log-output");if(!t)return;let e=this._getFilteredLogs();if(e.length===0){t.innerHTML='<div class="log-empty">No log output yet. Terminal will update when Loki Mode is running.</div>';return}t.innerHTML=e.map(i=>{let a=Et[i.level]||Et.info;return`
|
|
2313
2313
|
<div class="log-line">
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
package/web-app/server.py
CHANGED
|
@@ -685,25 +685,29 @@ def _run_loki_cmd(args: list, cwd: Optional[str] = None, timeout: int = 60) -> t
|
|
|
685
685
|
"""Run a loki CLI command and return (returncode, combined output).
|
|
686
686
|
|
|
687
687
|
Uses list form -- never shell=True with user input.
|
|
688
|
+
On timeout, the subprocess is explicitly killed to avoid orphaned processes.
|
|
688
689
|
"""
|
|
689
690
|
loki = _find_loki_cli()
|
|
690
691
|
if loki is None:
|
|
691
692
|
return (1, "loki CLI not found")
|
|
692
693
|
full_cmd = [loki] + args
|
|
693
694
|
try:
|
|
694
|
-
|
|
695
|
+
proc = subprocess.Popen(
|
|
695
696
|
full_cmd,
|
|
696
697
|
stdout=subprocess.PIPE,
|
|
697
698
|
stderr=subprocess.STDOUT,
|
|
698
699
|
stdin=subprocess.DEVNULL,
|
|
699
700
|
text=True,
|
|
700
701
|
cwd=cwd or session.project_dir or str(Path.home()),
|
|
701
|
-
timeout=timeout,
|
|
702
702
|
env={**os.environ},
|
|
703
703
|
)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
704
|
+
try:
|
|
705
|
+
stdout, _ = proc.communicate(timeout=timeout)
|
|
706
|
+
return (proc.returncode, stdout or "")
|
|
707
|
+
except subprocess.TimeoutExpired:
|
|
708
|
+
proc.kill()
|
|
709
|
+
proc.wait()
|
|
710
|
+
return (1, "Command timed out")
|
|
707
711
|
except Exception as e:
|
|
708
712
|
return (1, str(e))
|
|
709
713
|
|
|
@@ -1154,7 +1158,10 @@ async def _push_state_to_client(ws: WebSocket) -> None:
|
|
|
1154
1158
|
"""Background task: push state snapshots to a single WebSocket client.
|
|
1155
1159
|
|
|
1156
1160
|
Pushes every 2s when a session is running, every 30s when idle.
|
|
1161
|
+
Sends only incremental log deltas (new lines since last push) instead
|
|
1162
|
+
of the full log buffer each time.
|
|
1157
1163
|
"""
|
|
1164
|
+
last_log_index = max(len(session.log_lines) - 100, 0) # backfill handled on connect
|
|
1158
1165
|
while True:
|
|
1159
1166
|
is_running = (
|
|
1160
1167
|
session.process is not None
|
|
@@ -1214,10 +1221,12 @@ async def _push_state_to_client(ws: WebSocket) -> None:
|
|
|
1214
1221
|
except (json.JSONDecodeError, OSError):
|
|
1215
1222
|
pass
|
|
1216
1223
|
|
|
1217
|
-
# Build logs payload (
|
|
1218
|
-
|
|
1224
|
+
# Build incremental logs payload (only new lines since last push)
|
|
1225
|
+
current_len = len(session.log_lines)
|
|
1226
|
+
new_lines = session.log_lines[last_log_index:current_len] if current_len > last_log_index else []
|
|
1227
|
+
last_log_index = current_len
|
|
1219
1228
|
logs_payload = []
|
|
1220
|
-
for line in
|
|
1229
|
+
for line in new_lines:
|
|
1221
1230
|
level = "info"
|
|
1222
1231
|
lower = line.lower()
|
|
1223
1232
|
if "error" in lower or "fail" in lower:
|
|
@@ -1271,17 +1280,30 @@ async def websocket_endpoint(ws: WebSocket) -> None:
|
|
|
1271
1280
|
# Start server-push state task for this connection
|
|
1272
1281
|
push_task = asyncio.create_task(_push_state_to_client(ws))
|
|
1273
1282
|
|
|
1283
|
+
missed_pongs = 0
|
|
1274
1284
|
try:
|
|
1275
1285
|
while True:
|
|
1276
|
-
# Keep connection alive; handle client messages if needed
|
|
1277
|
-
data = await ws.receive_text()
|
|
1278
|
-
# Could handle commands here (e.g., stop session)
|
|
1279
1286
|
try:
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1287
|
+
data = await asyncio.wait_for(ws.receive_text(), timeout=60.0)
|
|
1288
|
+
missed_pongs = 0 # any message resets idle counter
|
|
1289
|
+
try:
|
|
1290
|
+
msg = json.loads(data)
|
|
1291
|
+
if msg.get("type") == "ping":
|
|
1292
|
+
await ws.send_text(json.dumps({"type": "pong"}))
|
|
1293
|
+
elif msg.get("type") == "pong":
|
|
1294
|
+
pass # client responded to our ping
|
|
1295
|
+
except json.JSONDecodeError:
|
|
1296
|
+
pass
|
|
1297
|
+
except asyncio.TimeoutError:
|
|
1298
|
+
# No message for 60s -- send a ping
|
|
1299
|
+
missed_pongs += 1
|
|
1300
|
+
if missed_pongs >= 2:
|
|
1301
|
+
# Two consecutive pings with no reply -- close idle connection
|
|
1302
|
+
break
|
|
1303
|
+
try:
|
|
1304
|
+
await ws.send_text(json.dumps({"type": "ping"}))
|
|
1305
|
+
except Exception:
|
|
1306
|
+
break
|
|
1285
1307
|
except WebSocketDisconnect:
|
|
1286
1308
|
pass
|
|
1287
1309
|
finally:
|