ltcai 3.4.0 → 3.5.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.
Files changed (56) hide show
  1. package/README.md +175 -225
  2. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +56 -0
  3. package/docs/assets/v3.4.1/e2e_runtime_log.txt +42 -0
  4. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  5. package/docs/assets/v3.4.1/local-agent.png +0 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/auth.py +37 -9
  8. package/latticeai/api/chat.py +6 -1
  9. package/latticeai/api/computer_use.py +21 -8
  10. package/latticeai/api/local_files.py +76 -10
  11. package/latticeai/api/tools.py +35 -35
  12. package/latticeai/core/agent.py +13 -2
  13. package/latticeai/core/builtin_hooks.py +106 -0
  14. package/latticeai/core/config.py +3 -0
  15. package/latticeai/core/hooks.py +76 -2
  16. package/latticeai/core/marketplace.py +1 -1
  17. package/latticeai/core/multi_agent.py +1 -1
  18. package/latticeai/core/oidc.py +205 -0
  19. package/latticeai/core/security.py +59 -5
  20. package/latticeai/core/workflow_engine.py +3 -3
  21. package/latticeai/core/workspace_os.py +1 -1
  22. package/latticeai/server_app.py +22 -34
  23. package/latticeai/services/platform_runtime.py +18 -6
  24. package/latticeai/services/tool_dispatch.py +2 -0
  25. package/latticeai/services/upload_service.py +24 -4
  26. package/local_knowledge_api.py +27 -1
  27. package/package.json +3 -3
  28. package/requirements.txt +1 -0
  29. package/scripts/check_python.py +87 -0
  30. package/static/css/reference/account.css +1 -1
  31. package/static/css/reference/admin.css +1 -1
  32. package/static/css/reference/base.css +8 -5
  33. package/static/css/reference/chat.css +8 -8
  34. package/static/css/reference/graph.css +2 -2
  35. package/static/css/responsive.css +2 -2
  36. package/static/v3/asset-manifest.json +9 -9
  37. package/static/v3/css/{lattice.shell.6ceea7c8.css → lattice.shell.8fcc9d33.css} +2 -1
  38. package/static/v3/css/lattice.shell.css +2 -1
  39. package/static/v3/js/{app.c4acfdd8.js → app.d086489d.js} +1 -1
  40. package/static/v3/js/core/{components.35f02e4c.js → components.f25b3b93.js} +1 -1
  41. package/static/v3/js/core/components.js +1 -1
  42. package/static/v3/js/core/{shell.80a6ad82.js → shell.d05266f5.js} +1 -1
  43. package/static/v3/js/views/{hooks.13845954.js → hooks.37895880.js} +12 -7
  44. package/static/v3/js/views/hooks.js +12 -7
  45. package/static/v3/js/views/{my-computer.c3ef5283.js → my-computer.d9d9ae1c.js} +7 -4
  46. package/static/v3/js/views/my-computer.js +7 -4
  47. package/static/workspace.css +1 -1
  48. package/tools/__init__.py +276 -0
  49. package/tools/commands.py +188 -0
  50. package/tools/computer.py +185 -0
  51. package/tools/documents.py +243 -0
  52. package/tools/filesystem.py +560 -0
  53. package/tools/knowledge.py +97 -0
  54. package/tools/local_files.py +69 -0
  55. package/tools/network.py +66 -0
  56. package/tools.py +0 -1525
@@ -37,6 +37,16 @@ async def process_uploaded_document(
37
37
  if not bytes_match_extension(contents, suffix):
38
38
  raise HTTPException(status_code=400, detail=f"파일 내용이 확장자({suffix})와 일치하지 않습니다.")
39
39
 
40
+ # ── pre_upload hook ── may gate the upload before any work happens.
41
+ if hooks is not None:
42
+ pre_up = hooks.fire_hook(
43
+ "pre_upload", "document.upload",
44
+ payload={"filename": file.filename, "ext": suffix, "bytes": len(contents)},
45
+ user_email=current_user,
46
+ )
47
+ if pre_up.get("blocked"):
48
+ raise HTTPException(status_code=403, detail=pre_up.get("block_reason") or "Upload blocked by a pre_upload hook.")
49
+
40
50
  with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
41
51
  tmp.write(contents)
42
52
  tmp_path = tmp.name
@@ -52,6 +62,12 @@ async def process_uploaded_document(
52
62
  },
53
63
  -1,
54
64
  )
65
+ # ── pre_index / post_index hooks bracket the KG ingest (chunk → embed →
66
+ # graph-build), the actual indexing step.
67
+ if hooks is not None:
68
+ hooks.fire_hook("pre_index", "document.index",
69
+ payload={"filename": file.filename, "chars": result.get("chars")},
70
+ user_email=current_user)
55
71
  try:
56
72
  if not (enable_graph and knowledge_graph):
57
73
  raise RuntimeError("graph disabled")
@@ -70,6 +86,12 @@ async def process_uploaded_document(
70
86
  except Exception as graph_error:
71
87
  logging.warning("knowledge graph document ingest failed: %s", graph_error)
72
88
  result["knowledge_graph"] = {"error": str(graph_error)}
89
+ if hooks is not None:
90
+ _kg = result.get("knowledge_graph") or {}
91
+ hooks.fire_hook("post_index", "document.index",
92
+ payload={"filename": file.filename, "graph_node": _kg.get("node_id"),
93
+ "indexed": bool(_kg.get("node_id")), "error": _kg.get("error")},
94
+ user_email=current_user)
73
95
 
74
96
  append_audit_event(
75
97
  "document_upload",
@@ -93,13 +115,11 @@ async def process_uploaded_document(
93
115
  except OSError:
94
116
  pass
95
117
 
96
- # ── pipeline hooks ── the ingestembedgraph-build pipeline just ran for
97
- # this document; fire the pipeline lifecycle hooks so downstream automation
98
- # (index-status publishing, custom reindex triggers) executes.
118
+ # ── post_upload hook ── the whole upload parseindex pipeline finished.
99
119
  if hooks is not None:
100
120
  kg = result.get("knowledge_graph") or {}
101
121
  hooks.fire_hook(
102
- "pipeline", "document.ingested",
122
+ "post_upload", "document.uploaded",
103
123
  payload={
104
124
  "filename": file.filename,
105
125
  "chars": result.get("chars"),
@@ -51,9 +51,10 @@ class _LocalWatchHandler:
51
51
  class LocalKnowledgeWatcher:
52
52
  """Debounced watchdog wrapper for approved local knowledge sources."""
53
53
 
54
- def __init__(self, get_graph: Callable[[], Any], *, debounce_seconds: float = 5.0):
54
+ def __init__(self, get_graph: Callable[[], Any], *, debounce_seconds: float = 5.0, hooks: Any = None):
55
55
  self._get_graph = get_graph
56
56
  self._debounce_seconds = debounce_seconds
57
+ self._hooks = hooks
57
58
  self._lock = threading.Lock()
58
59
  self._watched: Dict[str, Dict[str, Any]] = {}
59
60
  self._observer_cls = None
@@ -187,6 +188,10 @@ class LocalKnowledgeWatcher:
187
188
  if graph is None:
188
189
  return
189
190
  consent = source.get("consent") or {}
191
+ root = source.get("root_path")
192
+ if self._hooks is not None:
193
+ self._hooks.fire_hook("pre_index", "folder.reindex",
194
+ payload={"source_id": source_id, "root_path": root, "trigger": "watch"})
190
195
  try:
191
196
  graph.index_local_folder(
192
197
  Path(source["root_path"]),
@@ -199,11 +204,17 @@ class LocalKnowledgeWatcher:
199
204
  if source_id in self._watched:
200
205
  self._watched[source_id]["last_indexed_at"] = _now_seconds()
201
206
  self._watched[source_id]["last_error"] = None
207
+ if self._hooks is not None:
208
+ self._hooks.fire_hook("post_index", "folder.reindex",
209
+ payload={"source_id": source_id, "root_path": root, "trigger": "watch", "status": "ok"})
202
210
  except Exception as exc:
203
211
  logging.warning("local knowledge watcher reindex failed for %s: %s", source_id, exc)
204
212
  with self._lock:
205
213
  if source_id in self._watched:
206
214
  self._watched[source_id]["last_error"] = str(exc)
215
+ if self._hooks is not None:
216
+ self._hooks.fire_hook("post_index", "folder.reindex",
217
+ payload={"source_id": source_id, "root_path": root, "trigger": "watch", "status": "error", "error": str(exc)})
207
218
 
208
219
 
209
220
  def _now_seconds() -> float:
@@ -221,6 +232,7 @@ def create_local_knowledge_router(
221
232
  local_permission_response: Callable[..., dict],
222
233
  require_local_approval: Callable[..., None],
223
234
  watcher: Optional[LocalKnowledgeWatcher] = None,
235
+ hooks: Any = None,
224
236
  ) -> APIRouter:
225
237
  router = APIRouter()
226
238
 
@@ -293,6 +305,10 @@ def create_local_knowledge_router(
293
305
  if not req.approved:
294
306
  return local_permission_response(req.path, "read", current_user)
295
307
  require_local_approval(token=req.approval_token, path=req.path, action="read", user_email=current_user)
308
+ if hooks is not None:
309
+ hooks.fire_hook("pre_index", "folder.index",
310
+ payload={"root_path": req.path, "trigger": "connect", "watch": req.watch_enabled},
311
+ user_email=current_user)
296
312
  try:
297
313
  result = kg.index_local_folder(
298
314
  Path(req.path),
@@ -303,7 +319,17 @@ def create_local_knowledge_router(
303
319
  max_files=req.max_files,
304
320
  )
305
321
  except ValueError as exc:
322
+ if hooks is not None:
323
+ hooks.fire_hook("post_index", "folder.index",
324
+ payload={"root_path": req.path, "trigger": "connect", "status": "error", "error": str(exc)},
325
+ user_email=current_user)
306
326
  raise HTTPException(status_code=400, detail=str(exc)) from exc
327
+ if hooks is not None:
328
+ _idx = (result.get("index") or {}) if isinstance(result, dict) else {}
329
+ hooks.fire_hook("post_index", "folder.index",
330
+ payload={"root_path": req.path, "trigger": "connect", "status": "ok",
331
+ "indexed": _idx.get("indexed") or (result or {}).get("indexed")},
332
+ user_email=current_user)
307
333
 
308
334
  if watcher:
309
335
  if req.watch_enabled:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "build": "npm run build:assets && npm run build:python",
21
21
  "build:assets": "node scripts/build_v3_assets.mjs",
22
22
  "build:python": "python3 -m build",
23
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/auth.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py latticeai/api/mcp.py latticeai/core/hooks.py latticeai/api/hooks.py latticeai/core/agent_registry.py latticeai/api/agent_registry.py latticeai/services/memory_service.py latticeai/api/memory.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
+ "check:python": "python3 scripts/check_python.py",
24
24
  "lint": "node --check static/scripts/account.js && node --check static/scripts/admin.js && node --check static/scripts/chat.js && node --check static/scripts/graph.js && node --check static/scripts/platform.js && node --check static/scripts/ux.js && node --check static/scripts/workspace.js && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
25
25
  "lint:v3": "node scripts/lint_v3.mjs",
26
26
  "typecheck": "cd vscode-extension && npm run build",
@@ -70,7 +70,7 @@
70
70
  "llm_router.py",
71
71
  "p_reinforce.py",
72
72
  "telegram_bot.py",
73
- "tools.py",
73
+ "tools/",
74
74
  "codex_telegram_bot.py",
75
75
  "mcp_registry.py",
76
76
  "latticeai/**/*.py",
package/requirements.txt CHANGED
@@ -10,6 +10,7 @@ python-pptx
10
10
  python-multipart
11
11
  keyring
12
12
  authlib
13
+ cryptography
13
14
  pdfplumber
14
15
  pypdfium2
15
16
  watchdog
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """Discover-and-compile every first-party Python module.
3
+
4
+ Replaces the hand-maintained ``py_compile`` enumeration in CI and
5
+ ``package.json``: walks the repository, skips vendored / virtualenv / build /
6
+ cache / generated directories, and byte-compiles everything that remains. New
7
+ modules are picked up automatically — there is nothing to update when a file is
8
+ added, so the syntax gate can never silently fall behind the codebase.
9
+
10
+ Usage::
11
+
12
+ python scripts/check_python.py # compile all discovered modules
13
+ python scripts/check_python.py --list # just print what would be compiled
14
+
15
+ Exit code is non-zero if any module fails to compile.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import py_compile
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ ROOT = Path(__file__).resolve().parent.parent
25
+
26
+ # Directory names excluded anywhere in the tree: virtualenvs, build/cache
27
+ # artifacts, generated agent output, and vendored snapshots of older releases.
28
+ EXCLUDE_DIRS = {
29
+ ".git",
30
+ ".venv",
31
+ "venv",
32
+ "env",
33
+ ".build-venv",
34
+ ".npm-cache",
35
+ "build",
36
+ "dist",
37
+ "node_modules",
38
+ "__pycache__",
39
+ ".pytest_cache",
40
+ ".mypy_cache",
41
+ ".ruff_cache",
42
+ "agent_workspace",
43
+ "outputs",
44
+ "playwright-report",
45
+ "test-results",
46
+ "ltcai.egg-info",
47
+ ".ltcai",
48
+ ".ltcai-brain",
49
+ ".ltcai-test",
50
+ # Vendored snapshot of an older packaged release — not part of the build.
51
+ "ltcai-0.3.1",
52
+ }
53
+
54
+
55
+ def iter_modules():
56
+ for path in ROOT.rglob("*.py"):
57
+ parts = path.relative_to(ROOT).parts
58
+ if any(part in EXCLUDE_DIRS for part in parts):
59
+ continue
60
+ yield path
61
+
62
+
63
+ def main(argv: list[str]) -> int:
64
+ modules = sorted(iter_modules())
65
+ if "--list" in argv:
66
+ for path in modules:
67
+ print(path.relative_to(ROOT))
68
+ return 0
69
+
70
+ failures: list[str] = []
71
+ for path in modules:
72
+ try:
73
+ py_compile.compile(str(path), doraise=True)
74
+ except py_compile.PyCompileError as exc:
75
+ failures.append(str(exc))
76
+
77
+ if failures:
78
+ print("\n".join(failures))
79
+ print(f"check:python FAILED — {len(failures)} of {len(modules)} module(s) did not compile")
80
+ return 1
81
+
82
+ print(f"check:python OK — compiled {len(modules)} modules")
83
+ return 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ raise SystemExit(main(sys.argv[1:]))
@@ -117,7 +117,7 @@
117
117
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.9);
118
118
  position: relative;
119
119
  z-index: 1;
120
- backdrop-filter: blur(28px);
120
+ backdrop-filter: none; /* glass removed v3.5.0 */
121
121
  }
122
122
 
123
123
  .card::before {
@@ -45,7 +45,7 @@
45
45
  padding: 22px 28px;
46
46
  border-bottom: 1px solid rgba(111,66,232,0.10);
47
47
  background: var(--sidebar);
48
- backdrop-filter: blur(20px);
48
+ backdrop-filter: none; /* glass removed v3.5.0 */
49
49
  position: sticky;
50
50
  top: 0;
51
51
  z-index: 2;
@@ -46,7 +46,7 @@
46
46
  background: var(--card);
47
47
  border-color: var(--accent-soft);
48
48
  box-shadow: 0 20px 64px rgba(102, 82, 168, 0.20), inset 0 1px 0 rgba(255,255,255,0.86);
49
- backdrop-filter: blur(26px);
49
+ backdrop-filter: none; /* glass removed v3.5.0 */
50
50
  }
51
51
 
52
52
  .lattice-ref-auth .card::before {
@@ -65,7 +65,7 @@
65
65
  color: var(--ref-ink);
66
66
  background: var(--surface);
67
67
  border-bottom: 1px solid rgba(111,66,232,0.08);
68
- backdrop-filter: blur(12px);
68
+ backdrop-filter: none; /* glass removed v3.5.0 */
69
69
  }
70
70
 
71
71
  .auth-window-brand {
@@ -712,7 +712,10 @@
712
712
  font-weight: 700;
713
713
  background: transparent;
714
714
  cursor: pointer;
715
- transition: 160ms ease;
715
+ /* Animate surface affordances, but never `color`: a color transition on
716
+ theme switch would briefly render the rail link dark-on-dark (the v226
717
+ dark-mode contrast gate caught this on slow CI). Color flips instantly. */
718
+ transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
716
719
  }
717
720
 
718
721
  .reference-rail a.active,
@@ -842,7 +845,7 @@
842
845
  background: var(--sidebar);
843
846
  border-bottom: 1px solid rgba(111,66,232,0.11);
844
847
  box-shadow: 0 2px 12px rgba(88,72,150,0.06);
845
- backdrop-filter: blur(14px);
848
+ backdrop-filter: none; /* glass removed v3.5.0 */
846
849
  }
847
850
 
848
851
  .lattice-ref-chat .messages-viewport {
@@ -1295,7 +1298,7 @@
1295
1298
  height: 96px;
1296
1299
  background: var(--sidebar);
1297
1300
  border-bottom: 1px solid rgba(111,66,232,0.10);
1298
- backdrop-filter: blur(14px);
1301
+ backdrop-filter: none; /* glass removed v3.5.0 */
1299
1302
  padding: 22px 34px 8px;
1300
1303
  position: sticky;
1301
1304
  }
@@ -81,8 +81,8 @@
81
81
  display: flex;
82
82
  flex-direction: column;
83
83
  min-width: 240px;
84
- backdrop-filter: blur(24px);
85
- -webkit-backdrop-filter: blur(24px);
84
+ backdrop-filter: none; /* glass removed v3.5.0 */
85
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
86
86
  }
87
87
 
88
88
  .sidebar-header {
@@ -342,8 +342,8 @@
342
342
  padding: 10px 22px;
343
343
  border-bottom: 1px solid rgba(111,66,232,0.10);
344
344
  background: var(--sidebar);
345
- backdrop-filter: blur(20px);
346
- -webkit-backdrop-filter: blur(20px);
345
+ backdrop-filter: none; /* glass removed v3.5.0 */
346
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
347
347
  position: relative;
348
348
  z-index: 50;
349
349
  }
@@ -636,7 +636,7 @@
636
636
  justify-content: space-between;
637
637
  gap: 12px;
638
638
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.80);
639
- backdrop-filter: blur(10px);
639
+ backdrop-filter: none; /* glass removed v3.5.0 */
640
640
  transition: all .2s;
641
641
  }
642
642
 
@@ -1323,7 +1323,7 @@
1323
1323
  box-shadow: var(--shadow), 0 0 0 1px rgba(111,66,232,0.05);
1324
1324
  position: relative;
1325
1325
  z-index: 1;
1326
- backdrop-filter: blur(24px);
1326
+ backdrop-filter: none; /* glass removed v3.5.0 */
1327
1327
  }
1328
1328
 
1329
1329
  .auth-card::before {
@@ -2754,7 +2754,7 @@
2754
2754
  .admin-panel {
2755
2755
  background: var(--card);
2756
2756
  border-left: 1px solid var(--border);
2757
- backdrop-filter: blur(24px);
2757
+ backdrop-filter: none; /* glass removed v3.5.0 */
2758
2758
  color: var(--text);
2759
2759
  }
2760
2760
  .admin-header {
@@ -4254,7 +4254,7 @@
4254
4254
  background: var(--sidebar);
4255
4255
  border-bottom: 1px solid var(--line);
4256
4256
  box-shadow: none;
4257
- backdrop-filter: blur(14px);
4257
+ backdrop-filter: none; /* glass removed v3.5.0 */
4258
4258
  }
4259
4259
 
4260
4260
  /* Messages viewport */
@@ -120,7 +120,7 @@
120
120
  z-index: 20;
121
121
  border: 1px solid var(--line);
122
122
  background: var(--panel);
123
- backdrop-filter: blur(18px);
123
+ backdrop-filter: none; /* glass removed v3.5.0 */
124
124
  box-shadow: var(--shadow);
125
125
  }
126
126
 
@@ -904,7 +904,7 @@
904
904
  border-radius: 10px;
905
905
  background: var(--surface-2);
906
906
  box-shadow: var(--shadow);
907
- backdrop-filter: blur(18px);
907
+ backdrop-filter: none; /* glass removed v3.5.0 */
908
908
  }
909
909
 
910
910
  .focus-chip span {
@@ -319,8 +319,8 @@ select {
319
319
  z-index: 99;
320
320
  display: none;
321
321
  background: rgba(15, 12, 30, 0.42);
322
- -webkit-backdrop-filter: blur(2px);
323
- backdrop-filter: blur(2px);
322
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
323
+ backdrop-filter: none; /* glass removed v3.5.0 */
324
324
  }
325
325
  body.sidebar-open .sidebar-overlay { display: block; }
326
326
 
@@ -1,14 +1,14 @@
1
1
  {
2
- "version": "3.4.0",
2
+ "version": "3.5.0",
3
3
  "generated_at": "deterministic",
4
4
  "entrypoints": {
5
- "app": "/static/v3/js/app.c4acfdd8.js",
5
+ "app": "/static/v3/js/app.d086489d.js",
6
6
  "styles": [
7
7
  "/static/css/tokens.3ba22e37.css",
8
8
  "/static/v3/css/lattice.tokens.e7018963.css",
9
9
  "/static/v3/css/lattice.base.e4cdd05d.css",
10
10
  "/static/v3/css/lattice.components.9b49d614.css",
11
- "/static/v3/css/lattice.shell.6ceea7c8.css",
11
+ "/static/v3/css/lattice.shell.8fcc9d33.css",
12
12
  "/static/v3/css/lattice.views.22f69117.css"
13
13
  ]
14
14
  },
@@ -17,15 +17,15 @@
17
17
  "static/v3/css/lattice.tokens.css": "/static/v3/css/lattice.tokens.e7018963.css",
18
18
  "static/v3/css/lattice.base.css": "/static/v3/css/lattice.base.e4cdd05d.css",
19
19
  "static/v3/css/lattice.components.css": "/static/v3/css/lattice.components.9b49d614.css",
20
- "static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.6ceea7c8.css",
20
+ "static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.8fcc9d33.css",
21
21
  "static/v3/css/lattice.views.css": "/static/v3/css/lattice.views.22f69117.css",
22
- "static/v3/js/app.js": "/static/v3/js/app.c4acfdd8.js",
22
+ "static/v3/js/app.js": "/static/v3/js/app.d086489d.js",
23
23
  "static/v3/js/core/api.js": "/static/v3/js/core/api.12b568ad.js",
24
- "static/v3/js/core/components.js": "/static/v3/js/core/components.35f02e4c.js",
24
+ "static/v3/js/core/components.js": "/static/v3/js/core/components.f25b3b93.js",
25
25
  "static/v3/js/core/dom.js": "/static/v3/js/core/dom.a2773eb0.js",
26
26
  "static/v3/js/core/router.js": "/static/v3/js/core/router.584570f2.js",
27
27
  "static/v3/js/core/routes.js": "/static/v3/js/core/routes.d214b399.js",
28
- "static/v3/js/core/shell.js": "/static/v3/js/core/shell.80a6ad82.js",
28
+ "static/v3/js/core/shell.js": "/static/v3/js/core/shell.d05266f5.js",
29
29
  "static/v3/js/core/store.js": "/static/v3/js/core/store.34ebd5e6.js",
30
30
  "static/v3/js/views/admin-audit.js": "/static/v3/js/views/admin-audit.660a1fb1.js",
31
31
  "static/v3/js/views/admin-permissions.js": "/static/v3/js/views/admin-permissions.a7ae5f09.js",
@@ -37,14 +37,14 @@
37
37
  "static/v3/js/views/chat.js": "/static/v3/js/views/chat.e6dd7dd0.js",
38
38
  "static/v3/js/views/files.js": "/static/v3/js/views/files.adad14c1.js",
39
39
  "static/v3/js/views/home.js": "/static/v3/js/views/home.24f8b8ae.js",
40
- "static/v3/js/views/hooks.js": "/static/v3/js/views/hooks.13845954.js",
40
+ "static/v3/js/views/hooks.js": "/static/v3/js/views/hooks.37895880.js",
41
41
  "static/v3/js/views/hybrid-search.js": "/static/v3/js/views/hybrid-search.b22b97e0.js",
42
42
  "static/v3/js/views/knowledge-graph.js": "/static/v3/js/views/knowledge-graph.a14ea7e7.js",
43
43
  "static/v3/js/views/marketplace.js": "/static/v3/js/views/marketplace.ab0583d4.js",
44
44
  "static/v3/js/views/mcp.js": "/static/v3/js/views/mcp.99b5c6a7.js",
45
45
  "static/v3/js/views/memory.js": "/static/v3/js/views/memory.4ebdf474.js",
46
46
  "static/v3/js/views/models.js": "/static/v3/js/views/models.a1ffa147.js",
47
- "static/v3/js/views/my-computer.js": "/static/v3/js/views/my-computer.c3ef5283.js",
47
+ "static/v3/js/views/my-computer.js": "/static/v3/js/views/my-computer.d9d9ae1c.js",
48
48
  "static/v3/js/views/pipeline.js": "/static/v3/js/views/pipeline.c522f1ce.js",
49
49
  "static/v3/js/views/planning.js": "/static/v3/js/views/planning.9ac3e313.js",
50
50
  "static/v3/js/views/settings.js": "/static/v3/js/views/settings.8631fa5e.js",
@@ -349,8 +349,9 @@
349
349
  .lt3-scrim {
350
350
  position: fixed; inset: 0;
351
351
  z-index: var(--lt3-z-scrim);
352
+ /* Solid dim scrim — no backdrop blur (glassmorphism removed in v3.5.0 for a
353
+ crisp, stable surface). */
352
354
  background: var(--overlay);
353
- backdrop-filter: blur(2px);
354
355
  opacity: 0;
355
356
  animation: lt3-fade var(--lt3-dur-2) var(--lt3-ease) forwards;
356
357
  }
@@ -349,8 +349,9 @@
349
349
  .lt3-scrim {
350
350
  position: fixed; inset: 0;
351
351
  z-index: var(--lt3-z-scrim);
352
+ /* Solid dim scrim — no backdrop blur (glassmorphism removed in v3.5.0 for a
353
+ crisp, stable surface). */
352
354
  background: var(--overlay);
353
- backdrop-filter: blur(2px);
354
355
  opacity: 0;
355
356
  animation: lt3-fade var(--lt3-dur-2) var(--lt3-ease) forwards;
356
357
  }
@@ -3,7 +3,7 @@
3
3
  * Boots the shell. Views are lazy-loaded by the router (see core/routes.js).
4
4
  * ========================================================================== */
5
5
 
6
- import { boot } from "./core/shell.80a6ad82.js";
6
+ import { boot } from "./core/shell.d05266f5.js";
7
7
 
8
8
  const root = document.getElementById("app");
9
9
  if (root) boot(root);
@@ -74,7 +74,7 @@ const STATE_VARIANT = {
74
74
  // Agent runs / Hook dispatch). Keep these honest: amber for in-progress,
75
75
  // green for healthy/active, red for blocked/failed, neutral for inert.
76
76
  ingested: "warn", ingesting: "warn", watching: "ok", watched: "ok",
77
- connected: "ok", online: "ok", offline: "err", synced: "ok",
77
+ connected: "ok", online: "ok", offline: "err", degraded: "warn", starting: "warn", synced: "ok",
78
78
  queued: "warn", running: "warn", retrying: "warn", retried_ok: "ok",
79
79
  rejected: "err", cancelled: "", stopped: "", blocked: "err",
80
80
  advisory: "warn", skipped: "", complete: "ok", partial: "warn",
@@ -74,7 +74,7 @@ const STATE_VARIANT = {
74
74
  // Agent runs / Hook dispatch). Keep these honest: amber for in-progress,
75
75
  // green for healthy/active, red for blocked/failed, neutral for inert.
76
76
  ingested: "warn", ingesting: "warn", watching: "ok", watched: "ok",
77
- connected: "ok", online: "ok", offline: "err", synced: "ok",
77
+ connected: "ok", online: "ok", offline: "err", degraded: "warn", starting: "warn", synced: "ok",
78
78
  queued: "warn", running: "warn", retrying: "warn", retried_ok: "ok",
79
79
  rejected: "err", cancelled: "", stopped: "", blocked: "err",
80
80
  advisory: "warn", skipped: "", complete: "ok", partial: "warn",
@@ -8,7 +8,7 @@
8
8
  import { h, icon, $, $$ } from "./dom.a2773eb0.js";
9
9
  import { store } from "./store.34ebd5e6.js";
10
10
  import { api } from "./api.12b568ad.js";
11
- import * as c from "./components.35f02e4c.js";
11
+ import * as c from "./components.f25b3b93.js";
12
12
  import { createRouter } from "./router.584570f2.js";
13
13
  import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.d214b399.js";
14
14
 
@@ -1,13 +1,18 @@
1
1
  /* ============================================================================
2
- * View: Hooks — the lifecycle hooks registry.
3
- * Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
4
- * post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
5
- * registers custom hooks. Built-in hooks are platform-managed and labelled.
2
+ * View: Hooks — the lifecycle hooks registry + dispatch.
3
+ * Reads /api/hooks (built-in + user hooks across the pre_/post_ run, tool,
4
+ * workflow, upload, and index lifecycle pairs + agent), toggles enabled state,
5
+ * reorders, registers, runs hooks, and shows a recent-executions log. Built-in
6
+ * hooks are platform-managed; non-executable hooks are labelled "advisory".
6
7
  * ========================================================================== */
7
8
 
8
9
  const KIND_LABEL = {
9
- pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
10
- agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
10
+ pre_run: "Pre-run", post_run: "Post-run",
11
+ pre_tool: "Pre-tool", post_tool: "Post-tool",
12
+ pre_workflow: "Pre-workflow", post_workflow: "Post-workflow",
13
+ pre_upload: "Pre-upload", post_upload: "Post-upload",
14
+ pre_index: "Pre-index", post_index: "Post-index",
15
+ agent: "Agent",
11
16
  };
12
17
 
13
18
  export async function render(ctx) {
@@ -85,7 +90,7 @@ export async function render(ctx) {
85
90
  function hookRow(ctx2, hk) {
86
91
  return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
87
92
  h("div", { style: { "min-width": 0 } },
88
- h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
93
+ h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null, hk.advisory ? c.pill("advisory", "warn") : c.pill("executable", "ok")),
89
94
  h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
90
95
  hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
91
96
  ),
@@ -1,13 +1,18 @@
1
1
  /* ============================================================================
2
- * View: Hooks — the lifecycle hooks registry.
3
- * Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
4
- * post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
5
- * registers custom hooks. Built-in hooks are platform-managed and labelled.
2
+ * View: Hooks — the lifecycle hooks registry + dispatch.
3
+ * Reads /api/hooks (built-in + user hooks across the pre_/post_ run, tool,
4
+ * workflow, upload, and index lifecycle pairs + agent), toggles enabled state,
5
+ * reorders, registers, runs hooks, and shows a recent-executions log. Built-in
6
+ * hooks are platform-managed; non-executable hooks are labelled "advisory".
6
7
  * ========================================================================== */
7
8
 
8
9
  const KIND_LABEL = {
9
- pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
10
- agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
10
+ pre_run: "Pre-run", post_run: "Post-run",
11
+ pre_tool: "Pre-tool", post_tool: "Post-tool",
12
+ pre_workflow: "Pre-workflow", post_workflow: "Post-workflow",
13
+ pre_upload: "Pre-upload", post_upload: "Post-upload",
14
+ pre_index: "Pre-index", post_index: "Post-index",
15
+ agent: "Agent",
11
16
  };
12
17
 
13
18
  export async function render(ctx) {
@@ -85,7 +90,7 @@ export async function render(ctx) {
85
90
  function hookRow(ctx2, hk) {
86
91
  return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
87
92
  h("div", { style: { "min-width": 0 } },
88
- h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
93
+ h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null, hk.advisory ? c.pill("advisory", "warn") : c.pill("executable", "ok")),
89
94
  h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
90
95
  hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
91
96
  ),