loki-mode 7.92.0 → 7.93.0
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/dashboard/__init__.py +1 -1
- package/dashboard/server.py +179 -20
- package/dashboard/static/index.html +423 -104
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.93.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.93.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.93.0
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -667,7 +667,14 @@ async def _push_loki_state_loop() -> None:
|
|
|
667
667
|
"running_agents": running_agents,
|
|
668
668
|
"pending_tasks": len(pending) if isinstance(pending, list) else 0,
|
|
669
669
|
"current_task": in_prog[0].get("payload", {}).get("action", "") if isinstance(in_prog, list) and in_prog else "",
|
|
670
|
-
|
|
670
|
+
# Version reflects the RUNNING engine, not the project's
|
|
671
|
+
# stored state. raw.get("version") is whatever engine
|
|
672
|
+
# last wrote dashboard-state.json for THIS project (can
|
|
673
|
+
# be an old version, e.g. a project first built under
|
|
674
|
+
# 7.7.29), which made the displayed version flip between
|
|
675
|
+
# this stale value and the live one from the fallback
|
|
676
|
+
# path below. Always use the live engine version.
|
|
677
|
+
"version": _version,
|
|
671
678
|
}
|
|
672
679
|
await manager.broadcast({
|
|
673
680
|
"type": "status_update",
|
|
@@ -705,14 +712,10 @@ async def _push_loki_state_loop() -> None:
|
|
|
705
712
|
pass
|
|
706
713
|
|
|
707
714
|
if _sk_fresh:
|
|
708
|
-
#
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
try:
|
|
713
|
-
_sk_version = _vf.read_text().strip()
|
|
714
|
-
except OSError:
|
|
715
|
-
pass
|
|
715
|
+
# Version reflects the running engine (single source of
|
|
716
|
+
# truth), the same value the dashboard-state path uses
|
|
717
|
+
# above, so the displayed version never flips.
|
|
718
|
+
_sk_version = _version
|
|
716
719
|
|
|
717
720
|
# Read orchestrator state
|
|
718
721
|
_sk_phase = ""
|
|
@@ -1106,17 +1109,10 @@ async def get_status() -> StatusResponse:
|
|
|
1106
1109
|
loki_dir = _get_loki_dir()
|
|
1107
1110
|
uptime = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
1108
1111
|
|
|
1109
|
-
#
|
|
1110
|
-
version
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
version_file = os.path.join(project_root, "VERSION")
|
|
1114
|
-
if os.path.isfile(version_file):
|
|
1115
|
-
try:
|
|
1116
|
-
with open(version_file) as vf:
|
|
1117
|
-
version = vf.read().strip()
|
|
1118
|
-
except (OSError, IOError) as e:
|
|
1119
|
-
logger.warning(f"Failed to read VERSION file: {e}")
|
|
1112
|
+
# Version reflects the running engine (single source of truth: the package
|
|
1113
|
+
# __version__, same value every status path uses) so the displayed version is
|
|
1114
|
+
# always the live engine, never a stale per-project value.
|
|
1115
|
+
version = _version
|
|
1120
1116
|
|
|
1121
1117
|
# If .loki/ directory doesn't exist, return idle status immediately
|
|
1122
1118
|
if not loki_dir.is_dir():
|
|
@@ -8784,6 +8780,157 @@ def _reconcile_app_runner_liveness(state):
|
|
|
8784
8780
|
return state
|
|
8785
8781
|
|
|
8786
8782
|
|
|
8783
|
+
# Per-probe TCP connect timeout (seconds) for the single-process port probe.
|
|
8784
|
+
# Kept short so a stopped/firewalled port fails fast and the status endpoint
|
|
8785
|
+
# stays responsive for the dashboard's 3-5s pollers.
|
|
8786
|
+
_APP_RUNNER_PORT_PROBE_TIMEOUT = 1.0
|
|
8787
|
+
|
|
8788
|
+
|
|
8789
|
+
def _port_is_serving(port):
|
|
8790
|
+
"""True only if a TCP connection to 127.0.0.1:<port> genuinely succeeds.
|
|
8791
|
+
|
|
8792
|
+
Honest by construction: this proves *something* is accepting connections on
|
|
8793
|
+
the recorded port right now, never fabricates a result. Any failure (refused,
|
|
8794
|
+
timeout, bad port, OS error) returns False so the caller degrades to an
|
|
8795
|
+
honest non-running state. Synchronous; the caller offloads it to a worker
|
|
8796
|
+
thread so the event loop is never blocked by the connect.
|
|
8797
|
+
"""
|
|
8798
|
+
try:
|
|
8799
|
+
port = int(port)
|
|
8800
|
+
except (TypeError, ValueError):
|
|
8801
|
+
return False
|
|
8802
|
+
if port <= 0 or port > 65535:
|
|
8803
|
+
return False
|
|
8804
|
+
import socket
|
|
8805
|
+
for host in ("127.0.0.1", "::1"):
|
|
8806
|
+
sock = None
|
|
8807
|
+
try:
|
|
8808
|
+
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
|
8809
|
+
sock = socket.socket(family, socket.SOCK_STREAM)
|
|
8810
|
+
sock.settimeout(_APP_RUNNER_PORT_PROBE_TIMEOUT)
|
|
8811
|
+
if sock.connect_ex((host, port)) == 0:
|
|
8812
|
+
return True
|
|
8813
|
+
except OSError:
|
|
8814
|
+
continue
|
|
8815
|
+
finally:
|
|
8816
|
+
if sock is not None:
|
|
8817
|
+
try:
|
|
8818
|
+
sock.close()
|
|
8819
|
+
except OSError:
|
|
8820
|
+
pass
|
|
8821
|
+
return False
|
|
8822
|
+
|
|
8823
|
+
|
|
8824
|
+
def _dashboard_self_port():
|
|
8825
|
+
"""The TCP port this dashboard process itself is bound to, or None.
|
|
8826
|
+
|
|
8827
|
+
The user's app can never listen on the port the dashboard already occupies,
|
|
8828
|
+
so this is used to exclude a self-hit from the single-process probe (a stale
|
|
8829
|
+
recorded port that happens to equal the dashboard's port would otherwise
|
|
8830
|
+
probe-succeed against the dashboard and be misreported as the app running).
|
|
8831
|
+
Reads LOKI_DASHBOARD_PORT (the same env run_server binds), defaulting to the
|
|
8832
|
+
well-known 57374. Returns an int or None.
|
|
8833
|
+
"""
|
|
8834
|
+
try:
|
|
8835
|
+
return int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
|
|
8836
|
+
except (TypeError, ValueError):
|
|
8837
|
+
return 57374
|
|
8838
|
+
|
|
8839
|
+
|
|
8840
|
+
def _recorded_app_port(state):
|
|
8841
|
+
"""Best-effort recorded port for the single-process app, or None.
|
|
8842
|
+
|
|
8843
|
+
Reads the port the app-runner/CLI already recorded for THIS project, never a
|
|
8844
|
+
guessed value: first state.json's own `port`, then the app-runner
|
|
8845
|
+
detection.json the engine writes (`.loki/app-runner/detection.json`). Returns
|
|
8846
|
+
an int in the valid range or None. Pairing the probe to a *recorded* port is
|
|
8847
|
+
what keeps the result honest -- we never sweep arbitrary common ports.
|
|
8848
|
+
|
|
8849
|
+
Honesty guards:
|
|
8850
|
+
- A docker-compose detection.json is ignored here; a compose stack is the
|
|
8851
|
+
compose-discovery path's domain (which verifies the container belongs to
|
|
8852
|
+
the project), so feeding its port into the single-process probe would
|
|
8853
|
+
risk a false positive against an unrelated local service.
|
|
8854
|
+
- The dashboard's own port is never returned, since the user's app cannot
|
|
8855
|
+
bind it and a stale recorded value equal to it would self-hit the probe.
|
|
8856
|
+
"""
|
|
8857
|
+
self_port = _dashboard_self_port()
|
|
8858
|
+
|
|
8859
|
+
def _ok(p):
|
|
8860
|
+
try:
|
|
8861
|
+
p = int(p)
|
|
8862
|
+
except (TypeError, ValueError):
|
|
8863
|
+
return None
|
|
8864
|
+
if not (0 < p <= 65535):
|
|
8865
|
+
return None
|
|
8866
|
+
if self_port is not None and p == self_port:
|
|
8867
|
+
return None
|
|
8868
|
+
return p
|
|
8869
|
+
|
|
8870
|
+
if isinstance(state, dict):
|
|
8871
|
+
p = _ok(state.get("port"))
|
|
8872
|
+
if p is not None:
|
|
8873
|
+
return p
|
|
8874
|
+
try:
|
|
8875
|
+
det_file = _get_loki_dir() / "app-runner" / "detection.json"
|
|
8876
|
+
if det_file.is_file():
|
|
8877
|
+
det = json.loads(det_file.read_text())
|
|
8878
|
+
if isinstance(det, dict) and not det.get("is_docker"):
|
|
8879
|
+
p = _ok(det.get("port"))
|
|
8880
|
+
if p is not None:
|
|
8881
|
+
return p
|
|
8882
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
8883
|
+
pass
|
|
8884
|
+
return None
|
|
8885
|
+
|
|
8886
|
+
|
|
8887
|
+
def _discover_single_process_app_runner_state(state):
|
|
8888
|
+
"""Detect a genuinely-running single-process app via a recorded-port probe.
|
|
8889
|
+
|
|
8890
|
+
A SKILL/CLI-built project (e.g. one started with `npm run dev` outside
|
|
8891
|
+
app-runner.sh, or whose orchestrator has since exited) leaves a state.json
|
|
8892
|
+
with status "stopped"/"stale" and main_pid 0, even while the app itself is
|
|
8893
|
+
still serving. The dashboard would then report "not running" for a live app.
|
|
8894
|
+
|
|
8895
|
+
Resolution (all probe-verified, never fabricated):
|
|
8896
|
+
- Find the RECORDED port (state.json.port or non-docker detection.json) --
|
|
8897
|
+
never a guessed port, and never the dashboard's own port.
|
|
8898
|
+
- Probe 127.0.0.1:<port>. Only if the connection genuinely succeeds do we
|
|
8899
|
+
synthesize a "running" state using the recorded url/port.
|
|
8900
|
+
- Otherwise (no recorded port, or recorded port not reachable) return None;
|
|
8901
|
+
the caller keeps the honest reconciled state, which already carries
|
|
8902
|
+
positive evidence (the writer's settled "stopped", or a pid_gone /
|
|
8903
|
+
recycled / health_stale downgrade). "Unknown" would be less honest than
|
|
8904
|
+
that evidence, so we never override it here.
|
|
8905
|
+
|
|
8906
|
+
Synchronous and self-contained; the caller offloads it onto a worker thread.
|
|
8907
|
+
Never raises.
|
|
8908
|
+
"""
|
|
8909
|
+
try:
|
|
8910
|
+
port = _recorded_app_port(state)
|
|
8911
|
+
if not port:
|
|
8912
|
+
return None
|
|
8913
|
+
if not _port_is_serving(port):
|
|
8914
|
+
return None
|
|
8915
|
+
url = ""
|
|
8916
|
+
if isinstance(state, dict):
|
|
8917
|
+
url = state.get("url") or ""
|
|
8918
|
+
if not url:
|
|
8919
|
+
url = "http://localhost:{}".format(port)
|
|
8920
|
+
return {
|
|
8921
|
+
"status": "running",
|
|
8922
|
+
"url": url,
|
|
8923
|
+
"port": int(port),
|
|
8924
|
+
"method": (state.get("method") if isinstance(state, dict) else "") or "",
|
|
8925
|
+
"source": "probe",
|
|
8926
|
+
"externally_managed": True,
|
|
8927
|
+
"last_health": {"ok": True},
|
|
8928
|
+
}
|
|
8929
|
+
except Exception:
|
|
8930
|
+
# Fail open: never let the probe break the status endpoint.
|
|
8931
|
+
return None
|
|
8932
|
+
|
|
8933
|
+
|
|
8787
8934
|
# =============================================================================
|
|
8788
8935
|
# Docker-compose app-runner discovery
|
|
8789
8936
|
#
|
|
@@ -9191,6 +9338,10 @@ async def get_app_runner_status():
|
|
|
9191
9338
|
discovered = await asyncio.to_thread(_discover_compose_app_runner_state)
|
|
9192
9339
|
if discovered is not None:
|
|
9193
9340
|
return discovered
|
|
9341
|
+
# No state.json at all and no compose stack: there is no recorded port to
|
|
9342
|
+
# probe, so the single-process probe can only ever return an honest
|
|
9343
|
+
# "unknown" here. Keep returning not_initialized (the project has never
|
|
9344
|
+
# had an app-runner record) rather than overstating with "unknown".
|
|
9194
9345
|
return {"status": "not_initialized"}
|
|
9195
9346
|
|
|
9196
9347
|
try:
|
|
@@ -9208,6 +9359,14 @@ async def get_app_runner_status():
|
|
|
9208
9359
|
discovered = await asyncio.to_thread(_discover_compose_app_runner_state)
|
|
9209
9360
|
if discovered is not None:
|
|
9210
9361
|
return discovered
|
|
9362
|
+
|
|
9363
|
+
# Still nothing from compose: a SKILL/CLI-built project may be serving on its
|
|
9364
|
+
# recorded port even though no live app-runner.sh process owns it (main_pid 0
|
|
9365
|
+
# / orchestrator exited). Probe the RECORDED port and only report running when
|
|
9366
|
+
# the port genuinely answers; otherwise keep the honest reconciled state.
|
|
9367
|
+
probed = await asyncio.to_thread(_discover_single_process_app_runner_state, state)
|
|
9368
|
+
if isinstance(probed, dict) and probed.get("status") == "running":
|
|
9369
|
+
return probed
|
|
9211
9370
|
return reconciled
|
|
9212
9371
|
|
|
9213
9372
|
|
|
@@ -134,10 +134,17 @@
|
|
|
134
134
|
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/* Dashboard Layout
|
|
137
|
+
/* Dashboard Layout. v7.92 sidebar rebalance: a THREE-column grid -- a clean
|
|
138
|
+
left navigation rail, the main content, and a collapsible right status
|
|
139
|
+
sidebar. The left column is navigation only; the system status + session
|
|
140
|
+
controls moved out of the cramped left footer into the right column so
|
|
141
|
+
both surfaces can breathe. The third track is sized by a CSS var so the
|
|
142
|
+
same rule drives expanded (288px) and collapsed (a 48px rail). */
|
|
138
143
|
.dashboard-layout {
|
|
144
|
+
--status-rail-width: 48px;
|
|
145
|
+
--status-panel-width: 288px;
|
|
139
146
|
display: grid;
|
|
140
|
-
grid-template-columns: 240px 1fr;
|
|
147
|
+
grid-template-columns: 240px 1fr var(--status-panel-width);
|
|
141
148
|
grid-template-rows: 1fr;
|
|
142
149
|
/* Pin to the viewport (not min-height) so the grid row can never grow
|
|
143
150
|
taller than the screen. With min-height the 1fr row stretched to the
|
|
@@ -150,8 +157,16 @@
|
|
|
150
157
|
overflow: hidden;
|
|
151
158
|
}
|
|
152
159
|
|
|
160
|
+
/* Collapsed: the status sidebar shrinks to a thin rail and the center
|
|
161
|
+
content reclaims the freed width. Driven by a class on the GRID (not just
|
|
162
|
+
the aside) so the track resizes in lockstep with the panel. */
|
|
163
|
+
.dashboard-layout.status-collapsed {
|
|
164
|
+
grid-template-columns: 240px 1fr var(--status-rail-width);
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
@media (max-width: 768px) {
|
|
154
|
-
.dashboard-layout
|
|
168
|
+
.dashboard-layout,
|
|
169
|
+
.dashboard-layout.status-collapsed {
|
|
155
170
|
grid-template-columns: 1fr;
|
|
156
171
|
}
|
|
157
172
|
.sidebar { display: none; }
|
|
@@ -162,6 +177,18 @@
|
|
|
162
177
|
width: 240px;
|
|
163
178
|
z-index: 100;
|
|
164
179
|
}
|
|
180
|
+
/* On mobile the third column would steal the whole row; collapse it to a
|
|
181
|
+
pinned rail on the right edge instead so status stays one tap away
|
|
182
|
+
without crowding the content. */
|
|
183
|
+
.status-sidebar {
|
|
184
|
+
position: fixed;
|
|
185
|
+
right: 0; top: 0; bottom: 0;
|
|
186
|
+
width: var(--status-rail-width);
|
|
187
|
+
z-index: 90;
|
|
188
|
+
}
|
|
189
|
+
.dashboard-layout:not(.status-collapsed) .status-sidebar {
|
|
190
|
+
width: min(320px, 88vw);
|
|
191
|
+
}
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
/* Sidebar - glass effect. v7.84 enterprise IA: a three-region flex column
|
|
@@ -352,51 +379,56 @@
|
|
|
352
379
|
flex-shrink: 0;
|
|
353
380
|
}
|
|
354
381
|
|
|
355
|
-
/*
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
382
|
+
/* (v7.92) The left sidebar footer was removed: the session control + the
|
|
383
|
+
Settings gear it used to hold moved into the right status sidebar, so the
|
|
384
|
+
left column is navigation only.
|
|
385
|
+
|
|
386
|
+
(v7.93) The Settings gear + upward-opening popover were replaced by an
|
|
387
|
+
inline collapsible disclosure that lives in the right sidebar's scrolling
|
|
388
|
+
body (.status-settings). Inline-in-flow has no anchoring/clipping/z-index
|
|
389
|
+
failure modes, is keyboard-accessible, and matches the collapsible-sidebar
|
|
390
|
+
design language. The disclosure header is a real button (aria-expanded +
|
|
391
|
+
aria-controls); the region is hidden from the tab order while collapsed. */
|
|
392
|
+
|
|
393
|
+
/* Inline Settings disclosure (right-sidebar, in-flow). */
|
|
394
|
+
.status-settings {
|
|
362
395
|
flex: 0 0 auto;
|
|
396
|
+
border: 1px solid var(--loki-border);
|
|
397
|
+
border-radius: 8px;
|
|
398
|
+
background: var(--loki-bg-tertiary);
|
|
399
|
+
overflow: hidden;
|
|
363
400
|
}
|
|
364
401
|
|
|
365
|
-
|
|
402
|
+
/* Disclosure header: a full-width real button. The chevron rotates to point
|
|
403
|
+
down when the section is open. */
|
|
404
|
+
.settings-disclosure {
|
|
366
405
|
display: flex;
|
|
367
|
-
gap: 6px;
|
|
368
406
|
align-items: center;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
gap: 6px;
|
|
378
|
-
padding: 6px 10px;
|
|
379
|
-
background: var(--loki-bg-tertiary);
|
|
380
|
-
border: 1px solid var(--loki-border);
|
|
381
|
-
border-radius: 7px;
|
|
407
|
+
gap: 8px;
|
|
408
|
+
width: 100%;
|
|
409
|
+
box-sizing: border-box;
|
|
410
|
+
padding: 9px 11px;
|
|
411
|
+
background: transparent;
|
|
412
|
+
border: none;
|
|
413
|
+
border-radius: 8px;
|
|
414
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
382
415
|
font-size: 11px;
|
|
416
|
+
font-weight: 600;
|
|
417
|
+
text-transform: uppercase;
|
|
418
|
+
letter-spacing: 0.06em;
|
|
383
419
|
color: var(--loki-text-secondary);
|
|
384
420
|
cursor: pointer;
|
|
385
|
-
transition:
|
|
386
|
-
font-family: inherit;
|
|
421
|
+
transition: background var(--loki-transition), color var(--loki-transition);
|
|
387
422
|
}
|
|
388
|
-
|
|
389
|
-
.footer-btn:hover {
|
|
423
|
+
.settings-disclosure:hover {
|
|
390
424
|
background: var(--loki-bg-hover);
|
|
391
425
|
color: var(--loki-text-primary);
|
|
392
426
|
}
|
|
393
|
-
|
|
394
|
-
.footer-btn:focus-visible {
|
|
427
|
+
.settings-disclosure:focus-visible {
|
|
395
428
|
outline: 2px solid var(--loki-accent);
|
|
396
|
-
outline-offset:
|
|
429
|
+
outline-offset: -2px;
|
|
397
430
|
}
|
|
398
|
-
|
|
399
|
-
.footer-btn svg {
|
|
431
|
+
.settings-disclosure svg {
|
|
400
432
|
width: 14px;
|
|
401
433
|
height: 14px;
|
|
402
434
|
stroke: currentColor;
|
|
@@ -404,31 +436,56 @@
|
|
|
404
436
|
fill: none;
|
|
405
437
|
flex-shrink: 0;
|
|
406
438
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
439
|
+
.settings-disclosure .settings-gear {
|
|
440
|
+
margin-right: 2px;
|
|
441
|
+
}
|
|
442
|
+
.settings-disclosure .settings-chevron {
|
|
443
|
+
margin-left: auto;
|
|
444
|
+
transition: transform var(--loki-transition);
|
|
445
|
+
}
|
|
446
|
+
.settings-disclosure[aria-expanded="true"] .settings-chevron {
|
|
447
|
+
transform: rotate(90deg);
|
|
411
448
|
}
|
|
412
449
|
|
|
413
|
-
/*
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
box-shadow: var(--loki-glass-shadow);
|
|
426
|
-
z-index: 50;
|
|
450
|
+
/* Collapsible region. Uses the grid 0fr/1fr trick for a smooth height
|
|
451
|
+
transition; the inner wrapper is the actual clipping box. While collapsed
|
|
452
|
+
the region carries the [hidden] attribute (set by JS). We override the
|
|
453
|
+
default display:none [hidden] gives so the height can animate, but pair it
|
|
454
|
+
with visibility:hidden on the inner wrapper, which removes the input + Go
|
|
455
|
+
button from the tab order while collapsed (a plain 0fr collapse would
|
|
456
|
+
leave them tab-focusable). visibility is delayed on collapse and reset on
|
|
457
|
+
expand so the controls are reachable exactly when the section is open. */
|
|
458
|
+
.settings-region {
|
|
459
|
+
display: grid;
|
|
460
|
+
grid-template-rows: 1fr;
|
|
461
|
+
transition: grid-template-rows var(--loki-transition);
|
|
427
462
|
}
|
|
428
|
-
.settings-
|
|
429
|
-
display:
|
|
463
|
+
.settings-region[hidden] {
|
|
464
|
+
display: grid;
|
|
465
|
+
grid-template-rows: 0fr;
|
|
466
|
+
}
|
|
467
|
+
.settings-region-inner {
|
|
468
|
+
overflow: hidden;
|
|
469
|
+
min-height: 0;
|
|
470
|
+
visibility: visible;
|
|
471
|
+
transition: visibility 0s linear 0s;
|
|
472
|
+
}
|
|
473
|
+
.settings-region[hidden] .settings-region-inner {
|
|
474
|
+
visibility: hidden;
|
|
475
|
+
transition: visibility 0s linear 0.2s;
|
|
476
|
+
}
|
|
477
|
+
@media (prefers-reduced-motion: reduce) {
|
|
478
|
+
.settings-region[hidden] .settings-region-inner { transition: none; }
|
|
430
479
|
}
|
|
431
|
-
.settings-
|
|
480
|
+
.settings-region-body {
|
|
481
|
+
padding: 4px 11px 11px;
|
|
482
|
+
}
|
|
483
|
+
@media (prefers-reduced-motion: reduce) {
|
|
484
|
+
.settings-region { transition: none; }
|
|
485
|
+
.settings-disclosure .settings-chevron { transition: none; }
|
|
486
|
+
}
|
|
487
|
+
.settings-field-label {
|
|
488
|
+
display: block;
|
|
432
489
|
font-size: 9.5px;
|
|
433
490
|
font-weight: 600;
|
|
434
491
|
text-transform: uppercase;
|
|
@@ -436,7 +493,7 @@
|
|
|
436
493
|
color: var(--loki-text-muted);
|
|
437
494
|
margin-bottom: 6px;
|
|
438
495
|
}
|
|
439
|
-
.settings-
|
|
496
|
+
.settings-field-row {
|
|
440
497
|
display: flex;
|
|
441
498
|
gap: 6px;
|
|
442
499
|
align-items: center;
|
|
@@ -610,6 +667,147 @@
|
|
|
610
667
|
cursor: default;
|
|
611
668
|
}
|
|
612
669
|
|
|
670
|
+
/* Right status sidebar (v7.92). Mirrors the left sidebar's glass treatment
|
|
671
|
+
and three-region flex column (anchored header + scrolling body), but on
|
|
672
|
+
the right edge and collapsible. It owns its own scroll (min-height:0 +
|
|
673
|
+
overflow-y:auto on the body) so the tall status panel can never grow the
|
|
674
|
+
grid row and reintroduce the window-scroll bug documented above. */
|
|
675
|
+
.status-sidebar {
|
|
676
|
+
display: flex;
|
|
677
|
+
flex-direction: column;
|
|
678
|
+
background: var(--loki-glass-bg);
|
|
679
|
+
backdrop-filter: blur(16px) saturate(1.4);
|
|
680
|
+
-webkit-backdrop-filter: blur(16px) saturate(1.4);
|
|
681
|
+
border-left: 1px solid var(--loki-glass-border);
|
|
682
|
+
overflow: hidden;
|
|
683
|
+
min-height: 0;
|
|
684
|
+
height: 100vh;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* Anchored header: a small label + the collapse toggle. Does not scroll. */
|
|
688
|
+
.status-header {
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
justify-content: space-between;
|
|
692
|
+
gap: 8px;
|
|
693
|
+
padding: 16px 14px 12px;
|
|
694
|
+
flex: 0 0 auto;
|
|
695
|
+
border-bottom: 1px solid var(--loki-border-light);
|
|
696
|
+
}
|
|
697
|
+
.status-header-title {
|
|
698
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
699
|
+
font-size: 10px;
|
|
700
|
+
font-weight: 600;
|
|
701
|
+
text-transform: uppercase;
|
|
702
|
+
letter-spacing: 0.09em;
|
|
703
|
+
color: var(--loki-text-muted);
|
|
704
|
+
white-space: nowrap;
|
|
705
|
+
overflow: hidden;
|
|
706
|
+
text-overflow: ellipsis;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* Collapse/expand toggle: a small square icon button. The chevron points
|
|
710
|
+
right when expanded (click to collapse) and is swapped in the rail. */
|
|
711
|
+
.status-toggle {
|
|
712
|
+
display: inline-flex;
|
|
713
|
+
align-items: center;
|
|
714
|
+
justify-content: center;
|
|
715
|
+
width: 28px;
|
|
716
|
+
height: 28px;
|
|
717
|
+
flex: 0 0 auto;
|
|
718
|
+
padding: 0;
|
|
719
|
+
background: var(--loki-bg-tertiary);
|
|
720
|
+
border: 1px solid var(--loki-border);
|
|
721
|
+
border-radius: 7px;
|
|
722
|
+
color: var(--loki-text-secondary);
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
transition: background var(--loki-transition), color var(--loki-transition);
|
|
725
|
+
}
|
|
726
|
+
.status-toggle:hover {
|
|
727
|
+
background: var(--loki-bg-hover);
|
|
728
|
+
color: var(--loki-text-primary);
|
|
729
|
+
}
|
|
730
|
+
.status-toggle:focus-visible {
|
|
731
|
+
outline: 2px solid var(--loki-accent);
|
|
732
|
+
outline-offset: 1px;
|
|
733
|
+
}
|
|
734
|
+
.status-toggle svg {
|
|
735
|
+
width: 15px;
|
|
736
|
+
height: 15px;
|
|
737
|
+
stroke: currentColor;
|
|
738
|
+
stroke-width: 2;
|
|
739
|
+
fill: none;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* Scrolling body: holds the session-control component + the settings gear.
|
|
743
|
+
The only scroller in this column. */
|
|
744
|
+
.status-body {
|
|
745
|
+
flex: 1 1 auto;
|
|
746
|
+
min-height: 0;
|
|
747
|
+
overflow-y: auto;
|
|
748
|
+
overflow-x: hidden;
|
|
749
|
+
padding: 12px;
|
|
750
|
+
display: flex;
|
|
751
|
+
flex-direction: column;
|
|
752
|
+
gap: 10px;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Collapsed state: hide the expanded body AND the whole header (its collapse
|
|
756
|
+
chevron is meaningless once collapsed); the rail becomes the sole
|
|
757
|
+
affordance, so there is never a redundant pair of chevrons. */
|
|
758
|
+
.dashboard-layout.status-collapsed .status-body,
|
|
759
|
+
.dashboard-layout.status-collapsed .status-header {
|
|
760
|
+
display: none;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Collapsed rail: a thin vertical strip with the expand affordance and a
|
|
764
|
+
single glanceable live/connected dot. Hidden while expanded. */
|
|
765
|
+
.status-rail {
|
|
766
|
+
display: none;
|
|
767
|
+
flex-direction: column;
|
|
768
|
+
align-items: center;
|
|
769
|
+
gap: 14px;
|
|
770
|
+
padding: 14px 0;
|
|
771
|
+
flex: 1 1 auto;
|
|
772
|
+
min-height: 0;
|
|
773
|
+
}
|
|
774
|
+
.dashboard-layout.status-collapsed .status-rail {
|
|
775
|
+
display: flex;
|
|
776
|
+
}
|
|
777
|
+
.status-rail-label {
|
|
778
|
+
writing-mode: vertical-rl;
|
|
779
|
+
text-orientation: mixed;
|
|
780
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
781
|
+
font-size: 9.5px;
|
|
782
|
+
font-weight: 600;
|
|
783
|
+
text-transform: uppercase;
|
|
784
|
+
letter-spacing: 0.12em;
|
|
785
|
+
color: var(--loki-text-muted);
|
|
786
|
+
user-select: none;
|
|
787
|
+
}
|
|
788
|
+
/* Live dot on the rail. Reflects connection state via a class set by JS that
|
|
789
|
+
reads the shared API client (see initStatusSidebar). Never a static dot
|
|
790
|
+
implying liveness it does not have. */
|
|
791
|
+
.status-rail-dot {
|
|
792
|
+
width: 8px;
|
|
793
|
+
height: 8px;
|
|
794
|
+
border-radius: 50%;
|
|
795
|
+
background: var(--loki-text-muted);
|
|
796
|
+
flex: 0 0 auto;
|
|
797
|
+
}
|
|
798
|
+
.status-rail-dot.connected {
|
|
799
|
+
background: var(--loki-success);
|
|
800
|
+
animation: pulse 2s infinite;
|
|
801
|
+
}
|
|
802
|
+
@media (prefers-reduced-motion: reduce) {
|
|
803
|
+
.status-rail-dot.connected { animation: none; }
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* (v7.93) The anchored .status-footer that held the settings gear + its
|
|
807
|
+
upward-opening popover was removed: Settings is now an inline disclosure
|
|
808
|
+
inside the scrolling .status-body, so there is no separate footer to
|
|
809
|
+
anchor or hide on collapse. */
|
|
810
|
+
|
|
613
811
|
/* Main Content */
|
|
614
812
|
.main-content {
|
|
615
813
|
padding: 28px 32px;
|
|
@@ -927,28 +1125,10 @@
|
|
|
927
1125
|
</div>
|
|
928
1126
|
</nav>
|
|
929
1127
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
popover so it no longer clutters the always-visible footer. -->
|
|
935
|
-
<div class="footer-settings">
|
|
936
|
-
<button class="footer-btn" id="settings-btn" type="button" title="Settings" aria-label="Settings" aria-haspopup="true" aria-expanded="false">
|
|
937
|
-
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 110 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
938
|
-
<span>Settings</span>
|
|
939
|
-
</button>
|
|
940
|
-
<div class="settings-popover" id="settings-popover" role="dialog" aria-label="Settings">
|
|
941
|
-
<div class="settings-popover-label">API URL</div>
|
|
942
|
-
<div class="settings-popover-row">
|
|
943
|
-
<input type="text" class="api-url-input" id="api-url" placeholder="API URL">
|
|
944
|
-
<button class="api-btn" id="connect-btn" type="button">Go</button>
|
|
945
|
-
</div>
|
|
946
|
-
</div>
|
|
947
|
-
</div>
|
|
948
|
-
<!-- v7.90.1: dark mode removed (light-only). The theme toggle and its
|
|
949
|
-
handler are gone; the dashboard ships a single light theme. -->
|
|
950
|
-
</div>
|
|
951
|
-
</div>
|
|
1128
|
+
<!-- v7.92 sidebar rebalance: the system status + session controls moved
|
|
1129
|
+
out of this left footer into a dedicated, collapsible RIGHT sidebar
|
|
1130
|
+
(#status-sidebar) so the left column is clean navigation that can
|
|
1131
|
+
breathe. The settings gear moved with them. -->
|
|
952
1132
|
</aside>
|
|
953
1133
|
|
|
954
1134
|
<!-- Main Content -->
|
|
@@ -1414,6 +1594,67 @@
|
|
|
1414
1594
|
<loki-wiki-browser id="wiki-browser"></loki-wiki-browser>
|
|
1415
1595
|
</div>
|
|
1416
1596
|
</main>
|
|
1597
|
+
|
|
1598
|
+
<!-- v7.92 Right status sidebar: system status + session controls, moved out
|
|
1599
|
+
of the cramped left footer. Collapsible (state remembered per machine;
|
|
1600
|
+
default expanded). Collapsed -> a thin rail with an expand affordance
|
|
1601
|
+
and a single live/connected dot, so the info is never lost. -->
|
|
1602
|
+
<aside class="status-sidebar" id="status-sidebar" aria-label="System status and session controls">
|
|
1603
|
+
<div class="status-header">
|
|
1604
|
+
<span class="status-header-title">Session</span>
|
|
1605
|
+
<button class="status-toggle" id="status-toggle" type="button"
|
|
1606
|
+
aria-controls="status-sidebar" aria-expanded="true"
|
|
1607
|
+
title="Collapse status panel" aria-label="Collapse status panel">
|
|
1608
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|
1609
|
+
</button>
|
|
1610
|
+
</div>
|
|
1611
|
+
|
|
1612
|
+
<!-- Expanded body: the intact session-control component (status rows,
|
|
1613
|
+
Pause/Stop, model selector, Connected/version, agent/task counts) plus
|
|
1614
|
+
the inline Settings disclosure. Its API/polling wiring is preserved by
|
|
1615
|
+
moving the element whole. -->
|
|
1616
|
+
<div class="status-body">
|
|
1617
|
+
<loki-session-control id="session-control"></loki-session-control>
|
|
1618
|
+
|
|
1619
|
+
<!-- Settings: an inline collapsible disclosure (replaces the former
|
|
1620
|
+
floating gear popover, which mis-anchored in this column). The
|
|
1621
|
+
header is a real button driving aria-expanded + aria-controls; the
|
|
1622
|
+
region is [hidden] while collapsed so the API controls stay out of
|
|
1623
|
+
the tab order. Default collapsed - the API override is rarely used. -->
|
|
1624
|
+
<div class="status-settings">
|
|
1625
|
+
<button class="settings-disclosure" id="settings-disclosure" type="button"
|
|
1626
|
+
aria-expanded="false" aria-controls="settings-region">
|
|
1627
|
+
<svg class="settings-gear" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 110 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
|
1628
|
+
<span>Settings</span>
|
|
1629
|
+
<svg class="settings-chevron" viewBox="0 0 24 24" aria-hidden="true"><polyline points="9 6 15 12 9 18"/></svg>
|
|
1630
|
+
</button>
|
|
1631
|
+
<div class="settings-region" id="settings-region" hidden>
|
|
1632
|
+
<div class="settings-region-inner">
|
|
1633
|
+
<div class="settings-region-body">
|
|
1634
|
+
<label class="settings-field-label" for="api-url">API URL</label>
|
|
1635
|
+
<div class="settings-field-row">
|
|
1636
|
+
<input type="text" class="api-url-input" id="api-url" placeholder="API URL">
|
|
1637
|
+
<button class="api-btn" id="connect-btn" type="button">Go</button>
|
|
1638
|
+
</div>
|
|
1639
|
+
</div>
|
|
1640
|
+
</div>
|
|
1641
|
+
</div>
|
|
1642
|
+
</div>
|
|
1643
|
+
</div>
|
|
1644
|
+
|
|
1645
|
+
<!-- Collapsed rail: shown only when the column is collapsed. The expand
|
|
1646
|
+
button + a vertical label + a single live/connected dot wired to the
|
|
1647
|
+
shared API client (set in initStatusSidebar). -->
|
|
1648
|
+
<div class="status-rail" aria-hidden="false">
|
|
1649
|
+
<button class="status-toggle" id="status-expand" type="button"
|
|
1650
|
+
aria-controls="status-sidebar" aria-expanded="false"
|
|
1651
|
+
title="Expand status panel" aria-label="Expand status panel">
|
|
1652
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
|
1653
|
+
</button>
|
|
1654
|
+
<span class="status-rail-dot" id="status-rail-dot" title="Connection status" role="status" aria-label="Disconnected"></span>
|
|
1655
|
+
<span class="status-rail-label" aria-hidden="true">Status</span>
|
|
1656
|
+
</div>
|
|
1657
|
+
</aside>
|
|
1417
1658
|
</div>
|
|
1418
1659
|
|
|
1419
1660
|
<!-- Keyboard Shortcuts Help Overlay -->
|
|
@@ -3609,6 +3850,10 @@ var LokiDashboard=(()=>{var He=Object.defineProperty;var _t=Object.getOwnPropert
|
|
|
3609
3850
|
|
|
3610
3851
|
.model-select {
|
|
3611
3852
|
flex: 1;
|
|
3853
|
+
/* Allow the select to shrink below its longest option's intrinsic
|
|
3854
|
+
width (the flex min-content floor) so it never overflows a narrow
|
|
3855
|
+
sidebar column; the native control ellipsizes the label. */
|
|
3856
|
+
min-width: 0;
|
|
3612
3857
|
padding: 5px 8px;
|
|
3613
3858
|
border-radius: 4px;
|
|
3614
3859
|
border: 1px solid var(--loki-border);
|
|
@@ -15211,36 +15456,110 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
15211
15456
|
updateComponentsApiUrl(apiUrlInput.value);
|
|
15212
15457
|
});
|
|
15213
15458
|
|
|
15214
|
-
// v7.
|
|
15215
|
-
//
|
|
15216
|
-
//
|
|
15217
|
-
|
|
15218
|
-
|
|
15219
|
-
|
|
15220
|
-
|
|
15221
|
-
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15459
|
+
// v7.93 Settings disclosure: an inline collapsible section in the right
|
|
15460
|
+
// sidebar holding the API URL override (replaces the former floating popover).
|
|
15461
|
+
// The header button drives aria-expanded; the region toggles the [hidden]
|
|
15462
|
+
// attribute (which both animates the height via CSS and removes the API
|
|
15463
|
+
// controls from the tab order while collapsed). State is remembered per
|
|
15464
|
+
// machine. Best-effort -- no-ops on an older build without the elements.
|
|
15465
|
+
(function initSettingsDisclosure() {
|
|
15466
|
+
var btn = document.getElementById('settings-disclosure');
|
|
15467
|
+
var region = document.getElementById('settings-region');
|
|
15468
|
+
if (!btn || !region) return;
|
|
15469
|
+
var STORE_KEY = 'loki-settings-expanded';
|
|
15470
|
+
|
|
15471
|
+
function apply(expanded, persist) {
|
|
15472
|
+
btn.setAttribute('aria-expanded', String(expanded));
|
|
15473
|
+
region.hidden = !expanded;
|
|
15474
|
+
if (persist) {
|
|
15475
|
+
try { localStorage.setItem(STORE_KEY, expanded ? '1' : '0'); } catch (e) { /* ignore */ }
|
|
15476
|
+
}
|
|
15228
15477
|
}
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
if (popover.classList.contains('open')) close();
|
|
15238
|
-
});
|
|
15239
|
-
document.addEventListener('keydown', function(e) {
|
|
15240
|
-
if (e.key === 'Escape' && popover.classList.contains('open')) close();
|
|
15478
|
+
|
|
15479
|
+
var stored = null;
|
|
15480
|
+
try { stored = localStorage.getItem(STORE_KEY); } catch (e) { /* ignore */ }
|
|
15481
|
+
// Default collapsed: the API override is rarely used.
|
|
15482
|
+
apply(stored === '1', false);
|
|
15483
|
+
|
|
15484
|
+
btn.addEventListener('click', function() {
|
|
15485
|
+
apply(btn.getAttribute('aria-expanded') !== 'true', true);
|
|
15241
15486
|
});
|
|
15242
15487
|
})();
|
|
15243
15488
|
|
|
15489
|
+
// v7.92 Right status sidebar collapse. Toggled by the header chevron (when
|
|
15490
|
+
// expanded) and the rail chevron (when collapsed). State is remembered per
|
|
15491
|
+
// machine in localStorage; default is EXPANDED so nothing is hidden on first
|
|
15492
|
+
// load. Best-effort -- no-ops on an older build without the elements.
|
|
15493
|
+
(function initStatusSidebar() {
|
|
15494
|
+
var layout = document.querySelector('.dashboard-layout');
|
|
15495
|
+
var aside = document.getElementById('status-sidebar');
|
|
15496
|
+
var collapseBtn = document.getElementById('status-toggle');
|
|
15497
|
+
var expandBtn = document.getElementById('status-expand');
|
|
15498
|
+
if (!layout || !aside || !collapseBtn || !expandBtn) return;
|
|
15499
|
+
var STORE_KEY = 'loki-status-collapsed';
|
|
15500
|
+
|
|
15501
|
+
function syncAria(collapsed) {
|
|
15502
|
+
// aria-expanded describes the panel's state on whichever control is the
|
|
15503
|
+
// active affordance. Both controls point at the same panel.
|
|
15504
|
+
collapseBtn.setAttribute('aria-expanded', String(!collapsed));
|
|
15505
|
+
expandBtn.setAttribute('aria-expanded', String(!collapsed));
|
|
15506
|
+
}
|
|
15507
|
+
|
|
15508
|
+
function apply(collapsed, persist) {
|
|
15509
|
+
layout.classList.toggle('status-collapsed', collapsed);
|
|
15510
|
+
syncAria(collapsed);
|
|
15511
|
+
if (persist) {
|
|
15512
|
+
try { localStorage.setItem(STORE_KEY, collapsed ? '1' : '0'); } catch (e) { /* ignore */ }
|
|
15513
|
+
}
|
|
15514
|
+
}
|
|
15515
|
+
|
|
15516
|
+
function collapse() {
|
|
15517
|
+
apply(true, true);
|
|
15518
|
+
// Move focus to the rail's expand control so keyboard focus is never
|
|
15519
|
+
// orphaned on a now-hidden button.
|
|
15520
|
+
try { expandBtn.focus(); } catch (e) { /* ignore */ }
|
|
15521
|
+
}
|
|
15522
|
+
function expand() {
|
|
15523
|
+
apply(false, true);
|
|
15524
|
+
try { collapseBtn.focus(); } catch (e) { /* ignore */ }
|
|
15525
|
+
}
|
|
15526
|
+
|
|
15527
|
+
collapseBtn.addEventListener('click', collapse);
|
|
15528
|
+
expandBtn.addEventListener('click', expand);
|
|
15529
|
+
|
|
15530
|
+
// Restore remembered state (default expanded). Applied without persisting
|
|
15531
|
+
// and without stealing focus on load. On a narrow viewport the expanded
|
|
15532
|
+
// panel is a full overlay, so force the rail on load there (without
|
|
15533
|
+
// persisting -- the desktop preference is untouched); the user can still tap
|
|
15534
|
+
// expand to open the overlay and the header chevron to close it.
|
|
15535
|
+
var stored = null;
|
|
15536
|
+
try { stored = localStorage.getItem(STORE_KEY); } catch (e) { /* ignore */ }
|
|
15537
|
+
var startCollapsed = (window.innerWidth <= 768) ? true : (stored === '1');
|
|
15538
|
+
apply(startCollapsed, false);
|
|
15539
|
+
|
|
15540
|
+
// Glanceable live dot on the collapsed rail. Wired to the SAME shared API
|
|
15541
|
+
// client the session-control uses, so it reflects real connection state
|
|
15542
|
+
// rather than a static dot implying liveness it does not have.
|
|
15543
|
+
var dot = document.getElementById('status-rail-dot');
|
|
15544
|
+
if (dot && typeof LokiDashboard !== 'undefined' && LokiDashboard.getApiClient) {
|
|
15545
|
+
try {
|
|
15546
|
+
var api = LokiDashboard.getApiClient({ baseUrl: window.location.origin });
|
|
15547
|
+
function setConnected(connected) {
|
|
15548
|
+
dot.classList.toggle('connected', !!connected);
|
|
15549
|
+
dot.setAttribute('aria-label', connected ? 'Connected' : 'Disconnected');
|
|
15550
|
+
}
|
|
15551
|
+
api.addEventListener('api:connected', function () { setConnected(true); });
|
|
15552
|
+
api.addEventListener('api:disconnected', function () { setConnected(false); });
|
|
15553
|
+
api.addEventListener('api:status-update', function () { setConnected(true); });
|
|
15554
|
+
// Initial probe so the dot is honest before the first event arrives.
|
|
15555
|
+
if (typeof api.getStatus === 'function') {
|
|
15556
|
+
api.getStatus().then(function () { setConnected(true); })
|
|
15557
|
+
.catch(function () { setConnected(false); });
|
|
15558
|
+
}
|
|
15559
|
+
} catch (e) { /* leave the dot in its muted default */ }
|
|
15560
|
+
}
|
|
15561
|
+
})();
|
|
15562
|
+
|
|
15244
15563
|
// Offline detection
|
|
15245
15564
|
window.addEventListener('online', function() {
|
|
15246
15565
|
document.getElementById('offline-banner').classList.remove('show');
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.93.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.93.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.93.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=H8(G8(import.meta.url)),Z=e$(Q);Q$=W8(U8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var u1={};b(u1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>f1,commandVersion:()=>N8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>m1});async function f1($,Q=m1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([f1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=E8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function E8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function N8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var m1=16777216,Q1;var d=L(()=>{Q1=class Q1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return S8?"":$}var S8,T,S,_,lZ,I,k,y,J;var c=L(()=>{S8=(process.env.NO_COLOR??"").length>0;T=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),lZ=r("\x1B[0;34m"),I=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as u8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(u8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var q0={};b(q0,{runStatus:()=>W3});import{existsSync as v,readFileSync as U$,readdirSync as i1,statSync as e1}from"fs";import{resolve as D,basename as $3}from"path";import{homedir as Q3}from"os";function $0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Q0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=$0($),U=$0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function z3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -802,4 +802,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
802
802
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
803
803
|
`),process.stderr.write(Z8),2}}r1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var FZ=await jZ(Bun.argv.slice(2));process.exit(FZ);
|
|
804
804
|
|
|
805
|
-
//# debugId=
|
|
805
|
+
//# debugId=46A706C659684CEA64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.93.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.93.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|