context-graph-viewer 0.1.0__tar.gz
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.
- context_graph_viewer-0.1.0/.gitignore +189 -0
- context_graph_viewer-0.1.0/LICENSE +21 -0
- context_graph_viewer-0.1.0/PKG-INFO +87 -0
- context_graph_viewer-0.1.0/README.md +65 -0
- context_graph_viewer-0.1.0/pyproject.toml +48 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/__init__.py +9 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/app.py +146 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/base.py +210 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/cli.py +47 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/dashboard.py +353 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/failures.py +83 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/fmt.py +107 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/knowledge.py +454 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/queries.py +555 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/session.py +285 -0
- context_graph_viewer-0.1.0/src/context_graph_viewer/source.py +77 -0
- context_graph_viewer-0.1.0/tests/conftest.py +245 -0
- context_graph_viewer-0.1.0/tests/test_app.py +286 -0
- context_graph_viewer-0.1.0/tests/test_fmt.py +72 -0
- context_graph_viewer-0.1.0/tests/test_queries.py +295 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# VS Code
|
|
178
|
+
.vscode/
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Local agent hook configuration
|
|
182
|
+
/.codex/
|
|
183
|
+
/.claude/settings.local.json
|
|
184
|
+
|
|
185
|
+
# Project specific files
|
|
186
|
+
/enterprise-context/sic-agent/sic-scrapper/output/*
|
|
187
|
+
|
|
188
|
+
# deepeval writes run caches and telemetry here; build artifacts, not source.
|
|
189
|
+
.deepeval/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Memgraph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: context-graph-viewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only terminal viewer for a Context Graph: capture health, pipeline state, session anatomy and the knowledge graph
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: memgraph-context-graph>=0.6.0
|
|
13
|
+
Requires-Dist: memgraph-toolbox
|
|
14
|
+
Requires-Dist: textual<9,>=8.2
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: actions-graph[memgraph-context-graph]>=0.5.0; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'test'
|
|
18
|
+
Requires-Dist: pytest>=9.0.3; extra == 'test'
|
|
19
|
+
Requires-Dist: resources-graph[memgraph-context-graph]; extra == 'test'
|
|
20
|
+
Requires-Dist: sessions-graph[memgraph-context-graph]>=0.8.1; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# context-graph-viewer
|
|
24
|
+
|
|
25
|
+
A read-only terminal viewer for your Context Graph. It shows what your Claude Code / Codex hooks captured, whether
|
|
26
|
+
capture is failing, where reconciliation stands, and what the graph learned, without Cypher or Memgraph Lab.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
memgraph-context-graph ui
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
It reads the same config file as the hooks (`CONTEXT_GRAPH_CONFIG`, else `~/.config/context-graph/config.toml`),
|
|
33
|
+
polls Memgraph every few seconds, and never writes to the graph.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
`memgraph-context-graph setup` installs it unless you pass `--no-viewer`. It runs in the same tool environment as
|
|
38
|
+
`memgraph-context-graph`, so to add it by hand, add it to that tool's `uv tool install`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install "memgraph-context-graph[mcp]" \
|
|
42
|
+
--with "skills-graph[memgraph-context-graph]" \
|
|
43
|
+
--with "actions-graph[memgraph-context-graph]" \
|
|
44
|
+
--with "sessions-graph[memgraph-context-graph]" \
|
|
45
|
+
--with context-graph-viewer
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`context-graph-viewer` also installs its own command, which takes the same flags as `memgraph-context-graph ui`.
|
|
49
|
+
|
|
50
|
+
## Views
|
|
51
|
+
|
|
52
|
+
| View | Reached by | Shows |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **Dashboard** | start | Sessions in scope, the pipeline (reconcile and embedding queues, extraction backend, ontology version, failures, capture tokens), capture health (the local capture-failure log plus hints from the graph), and new entities per day. |
|
|
55
|
+
| **Session** | `enter` on a session | The timeline with subagents folded under the call that spawned them, each action's input and the resources it touched, and what the session produced (chunks, entities, memory files). |
|
|
56
|
+
| **Entities** | `k` on the dashboard, `g` on a session | Entities merged by name, by support (how many sessions and memory files ground them) or most recent, with their provenance. |
|
|
57
|
+
| **Neighbourhood** | `enter` on an entity | One entity's relations grouped by type, with the evidence behind each. `enter` on a neighbour moves there; `esc` walks back. |
|
|
58
|
+
|
|
59
|
+
## Keys
|
|
60
|
+
|
|
61
|
+
| Key | Does |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `a` | Widen the scope: your sessions → plus unattributed ones → everyone's |
|
|
64
|
+
| `p` | Narrow every view to one project (working directory) |
|
|
65
|
+
| `/` | Filter the table |
|
|
66
|
+
| `1`–`4` | Jump to a pane; `f` makes it fill the screen |
|
|
67
|
+
| `v` | Neighbourhood: relation tree or flat table |
|
|
68
|
+
| `+` / `-` | Neighbourhood: raise or lower the confidence floor |
|
|
69
|
+
| `h` / `s` | Entities: show value types (dates, amounts) / sort by most recent |
|
|
70
|
+
| `w` | Enterprise: next space this login can read |
|
|
71
|
+
| `r` | Refresh now |
|
|
72
|
+
| `?` | All keys |
|
|
73
|
+
| `esc` / `q` | Back / quit |
|
|
74
|
+
|
|
75
|
+
## Scope
|
|
76
|
+
|
|
77
|
+
By default you see your own sessions: `identity.user_id` in Community, your Memgraph login in Enterprise. Sessions
|
|
78
|
+
captured before a user was configured belong to nobody (reconcile files them under an `anon-…` user), so if the
|
|
79
|
+
dashboard opens empty, press `a`.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
Tests run against a real Memgraph and drive the app headless:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
./scripts/dev-memgraph.sh test context-graph-viewer
|
|
87
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# context-graph-viewer
|
|
2
|
+
|
|
3
|
+
A read-only terminal viewer for your Context Graph. It shows what your Claude Code / Codex hooks captured, whether
|
|
4
|
+
capture is failing, where reconciliation stands, and what the graph learned, without Cypher or Memgraph Lab.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
memgraph-context-graph ui
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
It reads the same config file as the hooks (`CONTEXT_GRAPH_CONFIG`, else `~/.config/context-graph/config.toml`),
|
|
11
|
+
polls Memgraph every few seconds, and never writes to the graph.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
`memgraph-context-graph setup` installs it unless you pass `--no-viewer`. It runs in the same tool environment as
|
|
16
|
+
`memgraph-context-graph`, so to add it by hand, add it to that tool's `uv tool install`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install "memgraph-context-graph[mcp]" \
|
|
20
|
+
--with "skills-graph[memgraph-context-graph]" \
|
|
21
|
+
--with "actions-graph[memgraph-context-graph]" \
|
|
22
|
+
--with "sessions-graph[memgraph-context-graph]" \
|
|
23
|
+
--with context-graph-viewer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`context-graph-viewer` also installs its own command, which takes the same flags as `memgraph-context-graph ui`.
|
|
27
|
+
|
|
28
|
+
## Views
|
|
29
|
+
|
|
30
|
+
| View | Reached by | Shows |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Dashboard** | start | Sessions in scope, the pipeline (reconcile and embedding queues, extraction backend, ontology version, failures, capture tokens), capture health (the local capture-failure log plus hints from the graph), and new entities per day. |
|
|
33
|
+
| **Session** | `enter` on a session | The timeline with subagents folded under the call that spawned them, each action's input and the resources it touched, and what the session produced (chunks, entities, memory files). |
|
|
34
|
+
| **Entities** | `k` on the dashboard, `g` on a session | Entities merged by name, by support (how many sessions and memory files ground them) or most recent, with their provenance. |
|
|
35
|
+
| **Neighbourhood** | `enter` on an entity | One entity's relations grouped by type, with the evidence behind each. `enter` on a neighbour moves there; `esc` walks back. |
|
|
36
|
+
|
|
37
|
+
## Keys
|
|
38
|
+
|
|
39
|
+
| Key | Does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `a` | Widen the scope: your sessions → plus unattributed ones → everyone's |
|
|
42
|
+
| `p` | Narrow every view to one project (working directory) |
|
|
43
|
+
| `/` | Filter the table |
|
|
44
|
+
| `1`–`4` | Jump to a pane; `f` makes it fill the screen |
|
|
45
|
+
| `v` | Neighbourhood: relation tree or flat table |
|
|
46
|
+
| `+` / `-` | Neighbourhood: raise or lower the confidence floor |
|
|
47
|
+
| `h` / `s` | Entities: show value types (dates, amounts) / sort by most recent |
|
|
48
|
+
| `w` | Enterprise: next space this login can read |
|
|
49
|
+
| `r` | Refresh now |
|
|
50
|
+
| `?` | All keys |
|
|
51
|
+
| `esc` / `q` | Back / quit |
|
|
52
|
+
|
|
53
|
+
## Scope
|
|
54
|
+
|
|
55
|
+
By default you see your own sessions: `identity.user_id` in Community, your Memgraph login in Enterprise. Sessions
|
|
56
|
+
captured before a user was configured belong to nobody (reconcile files them under an `anon-…` user), so if the
|
|
57
|
+
dashboard opens empty, press `a`.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
Tests run against a real Memgraph and drive the app headless:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
./scripts/dev-memgraph.sh test context-graph-viewer
|
|
65
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "context-graph-viewer"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Read-only terminal viewer for a Context Graph: capture health, pipeline state, session anatomy and the knowledge graph"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Programming Language :: Python :: 3.10",
|
|
10
|
+
"Programming Language :: Python :: 3.11",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
# Config, identity and spaces come from the hub, so the viewer reads exactly what the hooks write to.
|
|
16
|
+
"memgraph-context-graph>=0.6.0",
|
|
17
|
+
"memgraph-toolbox",
|
|
18
|
+
# Pinned to one major: Textual's maintenance has slowed and majors break widget APIs (#382).
|
|
19
|
+
"textual>=8.2,<9",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = [
|
|
24
|
+
"pytest>=9.0.3",
|
|
25
|
+
"pytest-asyncio>=0.26.0",
|
|
26
|
+
# The tests write sessions through the real hooks pipeline rather than hand-written Cypher.
|
|
27
|
+
"actions-graph[memgraph-context-graph]>=0.5.0",
|
|
28
|
+
"sessions-graph[memgraph-context-graph]>=0.8.1",
|
|
29
|
+
"resources-graph[memgraph-context-graph]",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
context-graph-viewer = "context_graph_viewer.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.uv.sources]
|
|
36
|
+
memgraph-context-graph = { workspace = true }
|
|
37
|
+
memgraph-toolbox = { workspace = true }
|
|
38
|
+
actions-graph = { workspace = true }
|
|
39
|
+
sessions-graph = { workspace = true }
|
|
40
|
+
resources-graph = { workspace = true }
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
asyncio_mode = "auto"
|
|
44
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Read-only terminal viewer for a Context Graph (map #374).
|
|
2
|
+
|
|
3
|
+
Run it with ``memgraph-context-graph ui``. It reads the hooks' config file, polls Memgraph, and never writes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .app import ViewerApp
|
|
7
|
+
from .cli import main
|
|
8
|
+
|
|
9
|
+
__all__ = ["ViewerApp", "main"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The viewer app: one reader per space, a scope shared by every view, and a polling tick."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
8
|
+
|
|
9
|
+
from textual.app import App
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
|
|
12
|
+
from . import fmt, queries
|
|
13
|
+
from .base import PolledScreen
|
|
14
|
+
from .dashboard import DashboardScreen
|
|
15
|
+
from .source import Identity, Reader, describe
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
POLL_SECONDS = 3.0
|
|
21
|
+
|
|
22
|
+
CSS = """
|
|
23
|
+
#left { width: 2fr; }
|
|
24
|
+
#sessions, #entities, #nb { height: 1fr; }
|
|
25
|
+
#side { width: 1fr; }
|
|
26
|
+
#banner { padding: 0 1; border: round $warning; height: auto; }
|
|
27
|
+
#filter { height: 3; }
|
|
28
|
+
.pane { border: round $primary-darken-2; padding: 0 1; height: 1fr; }
|
|
29
|
+
.pane:focus-within, .pane:focus { border: round $accent; }
|
|
30
|
+
#growth-pane { height: auto; }
|
|
31
|
+
#growth { height: 3; }
|
|
32
|
+
#timeline { height: 3fr; border: round $primary-darken-2; }
|
|
33
|
+
#timeline:focus { border: round $accent; }
|
|
34
|
+
#produced-pane { height: 1fr; max-height: 12; }
|
|
35
|
+
#main { width: 3fr; }
|
|
36
|
+
#details-pane { width: 2fr; }
|
|
37
|
+
#entities { width: 3fr; border: round $primary-darken-2; }
|
|
38
|
+
#entities:focus, #sessions:focus { border: round $accent; }
|
|
39
|
+
#sessions { border: round $primary-darken-2; }
|
|
40
|
+
#prov-pane { width: 2fr; }
|
|
41
|
+
#nbmain { width: 3fr; }
|
|
42
|
+
#nb { border: round $primary-darken-2; }
|
|
43
|
+
#nb:focus { border: round $accent; }
|
|
44
|
+
#evidence-pane { width: 2fr; }
|
|
45
|
+
#view-status { height: 1; padding: 0 1; background: $panel; }
|
|
46
|
+
ProjectPicker { align: center middle; }
|
|
47
|
+
#picker { width: 80%; max-width: 100; height: 70%; border: round $accent; background: $surface; padding: 0 1; }
|
|
48
|
+
#picker .title { text-style: bold; padding: 0 0 1 0; }
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ViewerApp(App[None]):
|
|
53
|
+
"""Read-only Context Graph viewer. Opens on the dashboard and polls the active view."""
|
|
54
|
+
|
|
55
|
+
TITLE = "Context Graph"
|
|
56
|
+
CSS = CSS
|
|
57
|
+
BINDINGS: ClassVar = [
|
|
58
|
+
Binding("a", "widen_scope", "scope"),
|
|
59
|
+
Binding("w", "next_space", "space", show=False),
|
|
60
|
+
Binding("question_mark", "help", "keys"),
|
|
61
|
+
Binding("q", "quit", "quit"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
identity: Identity,
|
|
67
|
+
*,
|
|
68
|
+
poll_seconds: float = POLL_SECONDS,
|
|
69
|
+
clock: Callable[[], datetime] | None = None,
|
|
70
|
+
reader_factory: Callable[..., Reader] = Reader,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Args:
|
|
73
|
+
identity: Who is looking, and which spaces they may read (personal first).
|
|
74
|
+
poll_seconds: How often the active view reloads; 0 turns polling off (tests drive it by hand).
|
|
75
|
+
clock: The current time, for "3m ago" text; tests pin it.
|
|
76
|
+
reader_factory: Builds the reader for a space.
|
|
77
|
+
"""
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.identity = identity
|
|
80
|
+
self.scope = queries.Scope(user_id=identity.user_id)
|
|
81
|
+
self.poll_seconds = poll_seconds
|
|
82
|
+
self._clock = clock or (lambda: datetime.now(timezone.utc))
|
|
83
|
+
self._reader_factory = reader_factory
|
|
84
|
+
self.space_index = 0
|
|
85
|
+
self.reader = reader_factory(identity.spaces[0])
|
|
86
|
+
|
|
87
|
+
def now(self) -> datetime:
|
|
88
|
+
"""The current time, from the injected clock."""
|
|
89
|
+
return self._clock()
|
|
90
|
+
|
|
91
|
+
def on_mount(self) -> None:
|
|
92
|
+
"""Open the dashboard and start polling."""
|
|
93
|
+
self.push_screen(DashboardScreen())
|
|
94
|
+
if self.poll_seconds > 0:
|
|
95
|
+
self.set_interval(self.poll_seconds, self.poll_active)
|
|
96
|
+
|
|
97
|
+
def on_unmount(self) -> None:
|
|
98
|
+
"""Close the connection."""
|
|
99
|
+
self.reader.close()
|
|
100
|
+
|
|
101
|
+
def poll_active(self) -> None:
|
|
102
|
+
"""Reload the view on top; views underneath catch up when they're shown again."""
|
|
103
|
+
if isinstance(self.screen, PolledScreen):
|
|
104
|
+
self.screen.action_poll()
|
|
105
|
+
|
|
106
|
+
def context_line(self) -> str:
|
|
107
|
+
"""Where, as whom, and what scope: shown under every view's title."""
|
|
108
|
+
space = self.identity.spaces[self.space_index]
|
|
109
|
+
user = self.identity.user_id or "no user configured"
|
|
110
|
+
parts = [f"{describe(space)}", f"{space.name} space", f"user {user}", f"scope {self.scope.mode}"]
|
|
111
|
+
if self.scope.project:
|
|
112
|
+
parts.append(f"project {fmt.project_name(self.scope.project)}")
|
|
113
|
+
return " · ".join(parts)
|
|
114
|
+
|
|
115
|
+
def _rescoped(self) -> None:
|
|
116
|
+
for screen in self.screen_stack:
|
|
117
|
+
if isinstance(screen, PolledScreen):
|
|
118
|
+
screen.refresh_titles()
|
|
119
|
+
self.poll_active()
|
|
120
|
+
|
|
121
|
+
def action_widen_scope(self) -> None:
|
|
122
|
+
"""Cycle the scope: your sessions, plus unattributed ones, everyone's."""
|
|
123
|
+
self.scope = self.scope.widened()
|
|
124
|
+
self._rescoped()
|
|
125
|
+
|
|
126
|
+
def set_project(self, project: str | None) -> None:
|
|
127
|
+
"""Narrow every view to one working directory, or None for all."""
|
|
128
|
+
self.scope = replace(self.scope, project=project)
|
|
129
|
+
self._rescoped()
|
|
130
|
+
|
|
131
|
+
def action_next_space(self) -> None:
|
|
132
|
+
"""Enterprise: switch to the next space this login may read; Community has only one."""
|
|
133
|
+
if len(self.identity.spaces) < 2:
|
|
134
|
+
self.notify("Only one space: shared spaces need the Enterprise edition.", timeout=3)
|
|
135
|
+
return
|
|
136
|
+
self.reader.close()
|
|
137
|
+
self.space_index = (self.space_index + 1) % len(self.identity.spaces)
|
|
138
|
+
self.reader = self._reader_factory(self.identity.spaces[self.space_index])
|
|
139
|
+
self._rescoped()
|
|
140
|
+
|
|
141
|
+
def action_help(self) -> None:
|
|
142
|
+
"""Toggle the key help panel."""
|
|
143
|
+
if self.screen.query("HelpPanel"):
|
|
144
|
+
self.action_hide_help_panel()
|
|
145
|
+
else:
|
|
146
|
+
self.action_show_help_panel()
|