loki-mode 7.7.28 → 7.7.29

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 CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.7.28
6
+ # Loki Mode v7.7.29
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.7.28 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.7.29 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.28
1
+ 7.7.29
package/autonomy/loki CHANGED
@@ -1593,12 +1593,19 @@ cmd_start() {
1593
1593
  exit 1
1594
1594
  fi
1595
1595
 
1596
- # --api flag: start the dashboard API server in background before the build
1596
+ # --api flag: start the dashboard API server before the build.
1597
+ # cmd_dashboard_start already daemonizes the server (nohup ... &) and runs
1598
+ # its own readiness probe, so run it in a contained SUBSHELL (not a
1599
+ # double-background) and surface the outcome. The old
1600
+ # `cmd_dashboard_start 2>/dev/null &` swallowed all output AND double-
1601
+ # backgrounded the already-daemonized server, so a port-in-use or startup
1602
+ # failure was invisible and the user got no dashboard and no error. The
1603
+ # subshell also contains cmd_dashboard_start's internal `exit 1` so a
1604
+ # dashboard failure can never abort the build.
1597
1605
  if [ "${LOKI_START_API:-false}" = "true" ]; then
1598
1606
  local dash_port="${LOKI_DASHBOARD_PORT:-57374}"
1599
1607
  echo -e "${GREEN}Starting dashboard API on port $dash_port...${NC}"
1600
- cmd_dashboard_start 2>/dev/null &
1601
- sleep 2
1608
+ ( cmd_dashboard_start ) || echo -e "${YELLOW}Dashboard API did not start (see message above); continuing the build without it.${NC}"
1602
1609
  fi
1603
1610
 
1604
1611
  # Phase F (v7.5.23): cross-project context discovery. Walks ONE parent
@@ -1896,17 +1903,23 @@ except (json.JSONDecodeError, OSError): pass
1896
1903
  # Clean up control files
1897
1904
  rm -f "$LOKI_DIR/STOP" "$LOKI_DIR/PAUSE" "$LOKI_DIR/PAUSED.md" 2>/dev/null
1898
1905
 
1899
- # Kill dashboard if running
1900
- if [ -f "$LOKI_DIR/dashboard/dashboard.pid" ]; then
1901
- local dash_pid
1902
- dash_pid=$(cat "$LOKI_DIR/dashboard/dashboard.pid" 2>/dev/null)
1903
- if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
1904
- kill "$dash_pid" 2>/dev/null || true
1905
- sleep 0.5
1906
- kill -9 "$dash_pid" 2>/dev/null || true
1906
+ # Kill dashboard if running. Check BOTH the project-local in-build
1907
+ # dashboard (.loki/dashboard) and the standalone one
1908
+ # (~/.loki/dashboard) so cleanup never leaves an orphan regardless of
1909
+ # which path started it.
1910
+ local _dash_pidf
1911
+ for _dash_pidf in "$LOKI_DIR/dashboard/dashboard.pid" "${HOME}/.loki/dashboard/dashboard.pid"; do
1912
+ if [ -f "$_dash_pidf" ]; then
1913
+ local dash_pid
1914
+ dash_pid=$(cat "$_dash_pidf" 2>/dev/null)
1915
+ if [ -n "$dash_pid" ] && kill -0 "$dash_pid" 2>/dev/null; then
1916
+ kill "$dash_pid" 2>/dev/null || true
1917
+ sleep 0.5
1918
+ kill -9 "$dash_pid" 2>/dev/null || true
1919
+ fi
1920
+ rm -f "$_dash_pidf"
1907
1921
  fi
1908
- rm -f "$LOKI_DIR/dashboard/dashboard.pid"
1909
- fi
1922
+ done
1910
1923
 
1911
1924
  # Kill any remaining registered processes (2s graceful window matches run.sh)
1912
1925
  if [ -d "$LOKI_DIR/pids" ]; then
@@ -2536,15 +2549,33 @@ if os.path.isfile(session_file):
2536
2549
  else:
2537
2550
  result['elapsed_time'] = 0
2538
2551
 
2539
- # Dashboard URL
2540
- dashboard_pid_file = os.path.join(loki_dir, 'dashboard', 'dashboard.pid')
2552
+ # Dashboard URL. Check both the project-local in-build dashboard and the
2553
+ # standalone dashboard (~/.loki/dashboard) and honor saved scheme/host/port
2554
+ # side-files, so --json reports the right URL regardless of which started it.
2555
+ _dash_candidates = [
2556
+ os.path.join(loki_dir, 'dashboard', 'dashboard.pid'),
2557
+ os.path.expanduser(os.path.join('~', '.loki', 'dashboard', 'dashboard.pid')),
2558
+ ]
2559
+ dashboard_pid_file = next((p for p in _dash_candidates if os.path.isfile(p)), _dash_candidates[0])
2541
2560
  dashboard_url = None
2542
2561
  if os.path.isfile(dashboard_pid_file):
2543
2562
  try:
2544
2563
  with open(dashboard_pid_file) as f:
2545
2564
  dpid = int(f.read().strip())
2546
2565
  os.kill(dpid, 0)
2547
- dashboard_url = 'http://127.0.0.1:' + dashboard_port + '/'
2566
+ _dd = os.path.dirname(dashboard_pid_file)
2567
+ def _side(name, default):
2568
+ p = os.path.join(_dd, name)
2569
+ try:
2570
+ return open(p).read().strip() if os.path.isfile(p) else default
2571
+ except OSError:
2572
+ return default
2573
+ _scheme = _side('scheme', 'http')
2574
+ _host = _side('host', '127.0.0.1')
2575
+ _port = _side('port', str(dashboard_port))
2576
+ if _host == '0.0.0.0':
2577
+ _host = '127.0.0.1'
2578
+ dashboard_url = _scheme + '://' + _host + ':' + _port + '/'
2548
2579
  except (ProcessLookupError, PermissionError, ValueError, Exception):
2549
2580
  pass
2550
2581
  result['dashboard_url'] = dashboard_url
@@ -3296,11 +3327,24 @@ cmd_provider_models() {
3296
3327
  done
3297
3328
  }
3298
3329
 
3299
- # Dashboard server management - project-local, unified with run.sh
3300
- DASHBOARD_PID_DIR="${LOKI_DIR}/dashboard"
3330
+ # Standalone dashboard server management (`loki dashboard`).
3331
+ # State lives at a FIXED, machine-global ~/.loki/dashboard so that
3332
+ # stop|status|open find the running server from ANY working directory.
3333
+ # (The relative ${LOKI_DIR}/dashboard path made these commands silently
3334
+ # fail from a different cwd than `start`, orphaning the server.) The
3335
+ # separate in-build dashboard started by run.sh during `loki start` stays
3336
+ # project-local and is unaffected by this constant.
3337
+ DASHBOARD_PID_DIR="${HOME}/.loki/dashboard"
3301
3338
  DASHBOARD_PID_FILE="${DASHBOARD_PID_DIR}/dashboard.pid"
3302
3339
  DASHBOARD_DEFAULT_PORT=57374
3303
- DASHBOARD_DEFAULT_HOST="127.0.0.1"
3340
+ # Default bind host: 0.0.0.0 inside a container (so a -p port map reaches
3341
+ # the server; 127.0.0.1 would be unreachable from the host), else loopback
3342
+ # for host safety. Detect Docker via /.dockerenv or LOKI_SANDBOX_MODE.
3343
+ if [ -f /.dockerenv ] || [ "${LOKI_SANDBOX_MODE:-}" = "true" ] || [ -n "${LOKI_IN_CONTAINER:-}" ]; then
3344
+ DASHBOARD_DEFAULT_HOST="0.0.0.0"
3345
+ else
3346
+ DASHBOARD_DEFAULT_HOST="127.0.0.1"
3347
+ fi
3304
3348
 
3305
3349
  cmd_dashboard() {
3306
3350
  local subcommand="${1:-}"
@@ -3546,12 +3590,19 @@ cmd_dashboard_start() {
3546
3590
  echo "$host" > "${DASHBOARD_PID_DIR}/host"
3547
3591
  echo "$url_scheme" > "${DASHBOARD_PID_DIR}/scheme"
3548
3592
 
3549
- # Wait for dashboard HTTP endpoint to become ready (up to 10 seconds)
3593
+ # Wait for the dashboard to become ready (up to 10 seconds). Probe the
3594
+ # UNAUTHENTICATED /health endpoint over the ACTUAL scheme: /api/status
3595
+ # 401s under LOKI_ENTERPRISE_AUTH, and the scheme is https when TLS is on,
3596
+ # so the old hardcoded http://.../api/status probe always failed against a
3597
+ # healthy TLS or auth-enabled server. Use a loopback-reachable host when
3598
+ # bound to 0.0.0.0, and -k to tolerate self-signed certs.
3599
+ local health_host="$host"; [ "$health_host" = "0.0.0.0" ] && health_host="127.0.0.1"
3600
+ local health_curl_opts="-sf"; [ "$url_scheme" = "https" ] && health_curl_opts="-sfk"
3550
3601
  local health_retries=20
3551
3602
  local health_interval=0.5
3552
3603
  local health_ok=false
3553
3604
  while [[ $health_retries -gt 0 ]]; do
3554
- if curl -sf "http://${host}:${port}/api/status" >/dev/null 2>&1; then
3605
+ if curl $health_curl_opts "${url_scheme}://${health_host}:${port}/health" >/dev/null 2>&1; then
3555
3606
  health_ok=true
3556
3607
  break
3557
3608
  fi
@@ -7802,11 +7853,37 @@ cmd_logs() {
7802
7853
  fi
7803
7854
  }
7804
7855
 
7805
- # API server management (delegates to unified FastAPI dashboard server)
7856
+ # API server management (delegates to unified FastAPI dashboard server).
7857
+ # Shares the SAME machine-global PID/control dir as `loki dashboard`
7858
+ # ($DASHBOARD_PID_DIR = ~/.loki/dashboard) so the two commands never fight
7859
+ # over the port and so stop/status/open are consistent across both. Parses
7860
+ # --host/--port, guards a busy port, computes the TLS scheme, and persists
7861
+ # host/port/scheme side-files like cmd_dashboard_start does.
7806
7862
  cmd_api() {
7807
7863
  local subcommand="${1:-help}"
7808
- local port="${LOKI_DASHBOARD_PORT:-57374}"
7809
- local pid_file="$LOKI_DIR/dashboard/dashboard.pid"
7864
+ shift || true
7865
+ local port="${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}"
7866
+ local host="${LOKI_DASHBOARD_HOST:-$DASHBOARD_DEFAULT_HOST}"
7867
+ local pid_file="$DASHBOARD_PID_FILE"
7868
+
7869
+ # Parse --host/--port (the old code ignored them, so `loki serve --port X`
7870
+ # silently bound 57374 and `--host 0.0.0.0` was dropped).
7871
+ while [ $# -gt 0 ]; do
7872
+ case "$1" in
7873
+ --port) port="${2:-$port}"; shift 2 ;;
7874
+ --port=*) port="${1#*=}"; shift ;;
7875
+ --host) host="${2:-$host}"; shift 2 ;;
7876
+ --host=*) host="${1#*=}"; shift ;;
7877
+ *) shift ;;
7878
+ esac
7879
+ done
7880
+
7881
+ local scheme="http"
7882
+ if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
7883
+ scheme="https"
7884
+ fi
7885
+ # Loopback-reachable host for printed URLs when bound to 0.0.0.0.
7886
+ local url_host="$host"; [ "$url_host" = "0.0.0.0" ] && url_host="127.0.0.1"
7810
7887
 
7811
7888
  case "$subcommand" in
7812
7889
  start)
@@ -7818,12 +7895,20 @@ cmd_api() {
7818
7895
 
7819
7896
  # Check if already running
7820
7897
  if [ -f "$pid_file" ]; then
7821
- local existing_pid=$(cat "$pid_file")
7822
- if kill -0 "$existing_pid" 2>/dev/null; then
7898
+ local existing_pid=$(cat "$pid_file" 2>/dev/null)
7899
+ if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
7823
7900
  echo -e "${YELLOW}Dashboard server already running (PID: $existing_pid)${NC}"
7824
- echo "URL: http://localhost:$port"
7901
+ echo "URL: ${scheme}://${url_host}:$port"
7825
7902
  exit 0
7826
7903
  fi
7904
+ rm -f "$pid_file"
7905
+ fi
7906
+
7907
+ # Refuse a busy port instead of writing a bogus PID.
7908
+ if command -v lsof &> /dev/null && lsof -i ":$port" &> /dev/null; then
7909
+ echo -e "${RED}Error: Port $port is already in use${NC}"
7910
+ echo "Stop the other server or set LOKI_DASHBOARD_PORT / --port to a free port."
7911
+ exit 1
7827
7912
  fi
7828
7913
 
7829
7914
  # Ensure dashboard Python dependencies
@@ -7833,20 +7918,24 @@ cmd_api() {
7833
7918
  fi
7834
7919
  local api_python="$DASHBOARD_PYTHON"
7835
7920
 
7836
- # Start server
7837
- mkdir -p "$LOKI_DIR/logs" "$LOKI_DIR/dashboard"
7838
- local host="${LOKI_DASHBOARD_HOST:-127.0.0.1}"
7921
+ # Start server. Persist host/port/scheme so status/open are accurate.
7922
+ mkdir -p "$LOKI_DIR/logs" "$DASHBOARD_PID_DIR"
7839
7923
  local uvicorn_args=("--host" "$host" "--port" "$port")
7840
- if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
7924
+ if [ "$scheme" = "https" ]; then
7841
7925
  uvicorn_args+=("--ssl-certfile" "${LOKI_TLS_CERT}" "--ssl-keyfile" "${LOKI_TLS_KEY}")
7842
7926
  fi
7843
- LOKI_DIR="$LOKI_DIR" PYTHONPATH="$SKILL_DIR" nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
7927
+ LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
7928
+ LOKI_DASHBOARD_HOST="$host" LOKI_DASHBOARD_PORT="$port" \
7929
+ nohup "$api_python" -m uvicorn dashboard.server:app "${uvicorn_args[@]}" > "$LOKI_DIR/logs/api.log" 2>&1 &
7844
7930
  local new_pid=$!
7845
7931
  echo "$new_pid" > "$pid_file"
7932
+ echo "$port" > "${DASHBOARD_PID_DIR}/port"
7933
+ echo "$host" > "${DASHBOARD_PID_DIR}/host"
7934
+ echo "$scheme" > "${DASHBOARD_PID_DIR}/scheme"
7846
7935
 
7847
7936
  echo -e "${GREEN}Dashboard server started${NC}"
7848
7937
  echo " PID: $new_pid"
7849
- echo " URL: http://localhost:$port"
7938
+ echo " URL: ${scheme}://${url_host}:$port"
7850
7939
  echo " Logs: $LOKI_DIR/logs/api.log"
7851
7940
  ;;
7852
7941
 
@@ -7870,15 +7959,22 @@ cmd_api() {
7870
7959
  if [ -f "$pid_file" ]; then
7871
7960
  local pid=$(cat "$pid_file")
7872
7961
  if kill -0 "$pid" 2>/dev/null; then
7962
+ # Prefer persisted side-files for the real bind.
7963
+ local s_scheme="$scheme" s_host="$url_host" s_port="$port"
7964
+ [ -f "${DASHBOARD_PID_DIR}/scheme" ] && s_scheme="$(cat "${DASHBOARD_PID_DIR}/scheme" 2>/dev/null)"
7965
+ [ -f "${DASHBOARD_PID_DIR}/host" ] && s_host="$(cat "${DASHBOARD_PID_DIR}/host" 2>/dev/null)"
7966
+ [ -f "${DASHBOARD_PID_DIR}/port" ] && s_port="$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)"
7967
+ [ "$s_host" = "0.0.0.0" ] && s_host="127.0.0.1"
7873
7968
  echo -e "${GREEN}Dashboard server running${NC}"
7874
7969
  echo " PID: $pid"
7875
- echo " URL: http://localhost:$port"
7970
+ echo " URL: ${s_scheme}://${s_host}:${s_port}"
7876
7971
 
7877
- # Try to fetch status
7972
+ # Try to fetch status (best-effort; may 401 under auth)
7878
7973
  if command -v curl &> /dev/null; then
7879
7974
  echo ""
7880
7975
  echo -e "${CYAN}Status:${NC}"
7881
- curl -s "http://localhost:$port/api/status" 2>/dev/null | jq . 2>/dev/null || true
7976
+ local _k=""; [ "$s_scheme" = "https" ] && _k="-k"
7977
+ curl -s $_k "${s_scheme}://${s_host}:${s_port}/api/status" 2>/dev/null | jq . 2>/dev/null || true
7882
7978
  fi
7883
7979
  else
7884
7980
  echo -e "${YELLOW}Dashboard server not running (stale PID file)${NC}"
package/autonomy/run.sh CHANGED
@@ -752,6 +752,47 @@ TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
752
752
  PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
753
753
  AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
754
754
 
755
+ # Multi-project registry (v7.7.29): register this running project in the
756
+ # machine-global registry (~/.loki/dashboard/projects.json) so the dashboard
757
+ # can list and switch between projects running in different folders. Records
758
+ # the absolute path, pid, port, and status. Fully non-blocking and
759
+ # failure-swallowed: registry problems must never affect a build. Marked
760
+ # inactive again on exit via the trap below.
761
+ loki_register_running_project() {
762
+ local _status="${1:-running}"
763
+ [ -n "${LOKI_SKIP_PROJECT_REGISTRY:-}" ] && return 0
764
+ command -v python3 >/dev/null 2>&1 || return 0
765
+ local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
766
+ LOKI_REG_TARGET="$TARGET_DIR" LOKI_REG_SKILL="$_skill" \
767
+ LOKI_REG_PID="$$" LOKI_REG_PORT="${LOKI_DASHBOARD_PORT:-57374}" \
768
+ LOKI_REG_STATUS="$_status" \
769
+ python3 - <<'PYREG' >/dev/null 2>&1 || true
770
+ import os, sys
771
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
772
+ try:
773
+ from dashboard import registry
774
+ target = os.path.abspath(os.environ["LOKI_REG_TARGET"])
775
+ entry = registry.register_project(target)
776
+ # Enrich with runtime fields the dashboard switcher uses.
777
+ reg = registry._load_registry()
778
+ pid = entry.get("id") or registry._generate_project_id(target)
779
+ if pid in reg.get("projects", {}):
780
+ reg["projects"][pid]["pid"] = int(os.environ.get("LOKI_REG_PID", "0") or 0)
781
+ reg["projects"][pid]["port"] = int(os.environ.get("LOKI_REG_PORT", "57374") or 57374)
782
+ reg["projects"][pid]["status"] = os.environ.get("LOKI_REG_STATUS", "running")
783
+ registry._save_registry(reg)
784
+ except Exception:
785
+ pass
786
+ PYREG
787
+ }
788
+ # Register as running now. We deliberately do NOT install an EXIT trap to
789
+ # flip it to idle: a top-level EXIT trap here would be clobbered by the
790
+ # lock-release EXIT trap installed later in the main path (and could
791
+ # interfere with it). Instead the dashboard determines live vs stale by
792
+ # checking whether the recorded pid is still alive (registry stores pid),
793
+ # which is robust even on hard kills where a trap would never fire.
794
+ loki_register_running_project running
795
+
755
796
  # Complexity Tiers (Auto-Claude pattern)
756
797
  # auto = detect from PRD/codebase, simple = 3 phases, standard = 6 phases, complex = 8 phases
757
798
  COMPLEXITY_TIER=${LOKI_COMPLEXITY:-auto}
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.28"
10
+ __version__ = "7.7.29"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2140,6 +2140,71 @@ async def clear_focus():
2140
2140
  return {"project_dir": None, "loki_dir": str(_get_loki_dir())}
2141
2141
 
2142
2142
 
2143
+ @app.get("/api/running-projects")
2144
+ async def list_running_projects():
2145
+ """List registered projects enriched with live status for the dashboard
2146
+ project switcher (v7.7.29 multi-project support).
2147
+
2148
+ NOTE: deliberately NOT under /api/projects/* because /api/projects/{id}
2149
+ (int path param) would shadow a /api/projects/running literal and 422.
2150
+
2151
+ Returns every registered project (from ~/.loki/dashboard/projects.json,
2152
+ populated by `loki start`), each annotated with:
2153
+ - running: whether the recorded orchestrator pid is still alive
2154
+ - is_active: whether it is the currently focused project
2155
+ Live-vs-stale is derived from pid liveness, which is robust even when a
2156
+ session is hard-killed (no exit hook fires). Never raises: registry
2157
+ problems degrade to an empty list.
2158
+ """
2159
+ out = []
2160
+ try:
2161
+ projects = registry.list_projects(include_inactive=True)
2162
+ except Exception:
2163
+ projects = []
2164
+ active = _active_project_dir
2165
+ for p in projects:
2166
+ path = p.get("path", "")
2167
+ pid = p.get("pid")
2168
+ running = False
2169
+ if isinstance(pid, int) and pid > 0:
2170
+ try:
2171
+ os.kill(pid, 0)
2172
+ running = True # signal 0 delivered -> pid alive
2173
+ except PermissionError:
2174
+ running = True # pid exists but owned by another user
2175
+ except (ProcessLookupError, OSError):
2176
+ running = False # ESRCH -> dead
2177
+ # A project is also "live" if its .loki/session.json says running.
2178
+ if not running and path:
2179
+ try:
2180
+ sess = _Path(path) / ".loki" / "session.json"
2181
+ if sess.is_file():
2182
+ import json as _json
2183
+ s = _json.loads(sess.read_text())
2184
+ running = s.get("status") == "running"
2185
+ except Exception:
2186
+ pass
2187
+ # Compare via realpath: /api/focus resolves symlinks (e.g. macOS
2188
+ # /tmp -> /private/tmp) while the registry stores abspath, so a plain
2189
+ # abspath compare would never match a focused symlinked project.
2190
+ is_active = False
2191
+ if active and path:
2192
+ try:
2193
+ is_active = os.path.realpath(active) == os.path.realpath(path)
2194
+ except OSError:
2195
+ is_active = os.path.abspath(active) == os.path.abspath(path)
2196
+ out.append({
2197
+ "id": p.get("id"),
2198
+ "name": p.get("name") or (os.path.basename(path) if path else "project"),
2199
+ "path": path,
2200
+ "port": p.get("port"),
2201
+ "status": p.get("status"),
2202
+ "running": running,
2203
+ "is_active": is_active,
2204
+ })
2205
+ return {"projects": out, "active_project_dir": active}
2206
+
2207
+
2143
2208
  # =============================================================================
2144
2209
  # Enterprise Features (Optional - enabled via environment variables)
2145
2210
  # =============================================================================
@@ -285,6 +285,25 @@
285
285
  box-shadow: 0 0 0 3px var(--loki-accent-glow);
286
286
  }
287
287
 
288
+ /* v7.7.29 multi-project switcher */
289
+ .project-switcher {
290
+ margin-left: 14px;
291
+ padding: 5px 10px;
292
+ background: var(--loki-bg-primary);
293
+ border: 1px solid var(--loki-border);
294
+ border-radius: 6px;
295
+ font-size: 12px;
296
+ font-family: 'Inter', system-ui, sans-serif;
297
+ color: var(--loki-text-primary);
298
+ cursor: pointer;
299
+ max-width: 280px;
300
+ }
301
+ .project-switcher:focus {
302
+ outline: none;
303
+ border-color: var(--loki-accent);
304
+ box-shadow: 0 0 0 3px var(--loki-accent-glow);
305
+ }
306
+
288
307
  /* Main Content */
289
308
  .main-content {
290
309
  padding: 28px 32px;
@@ -532,6 +551,11 @@
532
551
  </button>
533
552
  <span class="logo-brand">Loki Mode</span>
534
553
  <span class="logo-subtitle">powered by Autonomi</span>
554
+ <!-- v7.7.29 multi-project switcher: lists projects running loki in
555
+ different folders and switches which one the dashboard shows. -->
556
+ <select class="project-switcher" id="project-switcher" title="Switch project" aria-label="Switch project">
557
+ <option value="">All projects (current dir)</option>
558
+ </select>
535
559
  </div>
536
560
 
537
561
  <nav class="nav-links">
@@ -13373,6 +13397,49 @@ document.addEventListener('DOMContentLoaded', function() {
13373
13397
  var initResult = LokiDashboard.init({ autoDetectContext: true });
13374
13398
  console.log('Loki Dashboard initialized:', initResult);
13375
13399
 
13400
+ // v7.7.29 multi-project switcher: populate from /api/running-projects and
13401
+ // switch the focused project via /api/focus. Fully best-effort; if the
13402
+ // endpoint is unavailable the dropdown simply stays at "All projects".
13403
+ (function initProjectSwitcher() {
13404
+ var sel = document.getElementById('project-switcher');
13405
+ if (!sel) return;
13406
+ function refresh() {
13407
+ fetch('/api/running-projects')
13408
+ .then(function(r){ return r.ok ? r.json() : null; })
13409
+ .then(function(data){
13410
+ if (!data || !Array.isArray(data.projects)) return;
13411
+ var current = sel.value;
13412
+ // Rebuild options: keep the "All projects" default first.
13413
+ sel.innerHTML = '';
13414
+ var optAll = document.createElement('option');
13415
+ optAll.value = ''; optAll.textContent = 'All projects (current dir)';
13416
+ sel.appendChild(optAll);
13417
+ data.projects.forEach(function(p){
13418
+ var o = document.createElement('option');
13419
+ o.value = p.path || '';
13420
+ var dot = p.running ? '* ' : ''; // running marker (ASCII)
13421
+ o.textContent = dot + (p.name || p.path || 'project');
13422
+ if (p.is_active) o.selected = true;
13423
+ sel.appendChild(o);
13424
+ });
13425
+ if (!data.active_project_dir && current === '') sel.value = '';
13426
+ })
13427
+ .catch(function(){ /* offline / no endpoint: leave as-is */ });
13428
+ }
13429
+ sel.addEventListener('change', function(){
13430
+ var dir = sel.value;
13431
+ var req = dir
13432
+ ? fetch('/api/focus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_dir: dir }) })
13433
+ : fetch('/api/focus', { method: 'DELETE' });
13434
+ req.then(function(){
13435
+ // Reload so every panel re-fetches against the newly focused project.
13436
+ window.location.reload();
13437
+ }).catch(function(){ /* ignore */ });
13438
+ });
13439
+ refresh();
13440
+ setInterval(refresh, 15000);
13441
+ })();
13442
+
13376
13443
  // Theme toggle functionality
13377
13444
  var themeToggle = document.getElementById('theme-toggle');
13378
13445
  var themeLabel = document.getElementById('theme-label');
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.7.28
5
+ **Version:** v7.7.29
6
6
 
7
7
  ---
8
8