Document current coding-workflow stack state
Snapshot of where opencode + Qwen3-Coder + MCPs + Kimi-Linear + voice + Phoenix tracing land today, plus in-flight (oc-tree, kimi-linear context ramp) and next (ComfyUI) items with pointers to per-project NEXT_STEPS.md guides.
This commit is contained in:
1
oc-tree/.python-version
Normal file
1
oc-tree/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
120
oc-tree/NEXT_STEPS.md
Normal file
120
oc-tree/NEXT_STEPS.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# oc-tree — resumption guide
|
||||
|
||||
Open this file first when picking the work back up.
|
||||
|
||||
## What this project is
|
||||
|
||||
A Python TUI sidecar that subscribes to `opencode serve`'s SSE event
|
||||
stream and renders a live tree of sessions → messages → tool calls in a
|
||||
terminal pane next to the opencode TUI. Subagents nest under their
|
||||
parent via `session.created.info.parentID`.
|
||||
|
||||
Roadmap entry: `localgenai/Roadmap.md` → "Layer 0: Cross-cutting
|
||||
capabilities" → "Observability — partial" + prioritized step #12.
|
||||
|
||||
Phoenix (`opencode/.opencode/plugin/phoenix-bridge.js`) already handles
|
||||
the *external* deep-trace store. oc-tree is the glanceable "what's it
|
||||
doing right now" pane that lives in-harness (well, in tmux next to it).
|
||||
|
||||
## Where we are
|
||||
|
||||
- Plan agreed: Python + textual, lives at `localgenai/oc-tree/`.
|
||||
- Phases: M0 → M1 → M2 → M3 → M4. See descriptions in this repo's task
|
||||
list (`TaskList`) or the roadmap entry.
|
||||
- **M0 — DONE** (skeleton). `uv sync` installs cleanly; `oc-tree` and
|
||||
`oc-tree-probe` entry points resolve. Imports verified.
|
||||
- **M0 — AWAITING USER ACTION** (schema verification). Probe is built
|
||||
but hasn't been run against a live `opencode serve`. Three open
|
||||
schema questions still unanswered.
|
||||
|
||||
## What blocks progress
|
||||
|
||||
User needs to run the probe against a real opencode session and report
|
||||
back. Without these answers, M2's reducer design is guesswork.
|
||||
|
||||
Run in a tmux pane while `opencode serve` is up:
|
||||
|
||||
```sh
|
||||
cd ~/Documents/obsidian/localgenai/oc-tree
|
||||
uv run oc-tree-probe
|
||||
```
|
||||
|
||||
Drive opencode through a session that hits **all three** triggers:
|
||||
- Spawn a Task-tool subagent
|
||||
- Trigger at least one permission prompt
|
||||
- Make at least one regular tool call (Read/Bash/etc.)
|
||||
|
||||
Ctrl-C the probe. Then run:
|
||||
|
||||
```sh
|
||||
# Q1: does session.created.info.parentID populate for subagents?
|
||||
jq -r 'select(.type=="session.created") | .raw.properties.info.parentID' \
|
||||
/tmp/oc-tree-probe.jsonl
|
||||
|
||||
# Q2: does message.part.updated carry full part or delta?
|
||||
jq -c 'select(.type=="message.part.updated") | .raw.properties.part' \
|
||||
/tmp/oc-tree-probe.jsonl | head
|
||||
|
||||
# Q3: what permission.* events actually fire?
|
||||
jq -r '.type' /tmp/oc-tree-probe.jsonl | grep -i permission | sort -u
|
||||
```
|
||||
|
||||
Paste the output (or the JSONL file path) into the next session.
|
||||
|
||||
## What happens next
|
||||
|
||||
Once probe answers are in:
|
||||
|
||||
1. Mark M0 complete, start **M1 (flat session list)** — textual app,
|
||||
live-updating list of sessions with status, no nesting yet. Proves
|
||||
the reducer + render loop. Independent of the schema answers, so
|
||||
could start in parallel.
|
||||
2. **M2 (tree view)** — needs probe answers to know:
|
||||
- Whether to nest by `parentID` directly (Q1 yes) or fall back to
|
||||
inferring subagents from `Task` tool-part response payloads.
|
||||
- Whether the part-update reducer replaces by `partID` (Q2 = full
|
||||
part) or merges a delta (Q2 = delta).
|
||||
- What permission events to render (Q3).
|
||||
3. **M3 (reconnect + state rebuild)** — heartbeat watchdog, REST replay
|
||||
on disconnect. Driven by sst/opencode#15149/#22198 known leaks.
|
||||
4. **M4 (polish)** — keybindings, theme, tmux layout doc.
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
localgenai/oc-tree/
|
||||
├── pyproject.toml uv project (textual, httpx, httpx-sse)
|
||||
├── README.md user-facing readme
|
||||
├── NEXT_STEPS.md this file
|
||||
├── .python-version 3.11
|
||||
└── src/oc_tree/
|
||||
├── client.py OpenCodeClient: REST + SSE
|
||||
├── probe.py schema-verification CLI
|
||||
├── __main__.py stub for `oc-tree` (real TUI in M1)
|
||||
└── widgets/ empty (populated in M1+)
|
||||
```
|
||||
|
||||
## Key references
|
||||
|
||||
- opencode server docs: <https://opencode.ai/docs/server/>
|
||||
- Authoritative schema: `GET /doc` on a running `opencode serve` (do
|
||||
not hardcode — fetch per-version).
|
||||
- sst/opencode#7451 — no per-session SSE endpoint; we filter `/event`
|
||||
client-side.
|
||||
- sst/opencode#6573 — Task subagent over `opencode serve` may have
|
||||
bugs; this is what Q1 verifies.
|
||||
- sst/opencode#11424 — `message.part.updated` sometimes replays full
|
||||
state; this is what Q2 verifies.
|
||||
- sst/opencode#15149, #22198 — SSE disconnect leaks; informs M3
|
||||
shutdown discipline.
|
||||
|
||||
## Decisions worth not relitigating
|
||||
|
||||
- **Python + textual** chosen over Go+Bubbletea (faster iteration,
|
||||
matches stack — uvx already in use) and Node+ink (worse SSE/UI
|
||||
ergonomics; phoenix-bridge.js doesn't justify matching).
|
||||
- **Read-only v1.** No sending messages, no editing. Just visibility.
|
||||
- **Lives in `localgenai/oc-tree/`** rather than its own repo; can be
|
||||
extracted later if it warrants a standalone release.
|
||||
- **State rebuild via REST on every (re)connect** rather than trusting
|
||||
SSE catchup or `Last-Event-ID` (server doesn't honor it).
|
||||
72
oc-tree/README.md
Normal file
72
oc-tree/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# oc-tree
|
||||
|
||||
Live tree-view sidecar for [opencode](https://opencode.ai). Subscribes
|
||||
to the `opencode serve` SSE event stream and renders a live hierarchy of
|
||||
sessions → messages → tool calls in a terminal pane next to the opencode
|
||||
TUI.
|
||||
|
||||
Phoenix (`opencode/.opencode/plugin/phoenix-bridge.js`) handles
|
||||
after-the-fact deep traces; oc-tree is the glanceable "what's it doing
|
||||
right now" pane.
|
||||
|
||||
## Status
|
||||
|
||||
- **M0** — skeleton + schema probe (current).
|
||||
- M1 — flat session list (textual).
|
||||
- M2 — tree view with subagent nesting via `parentID`.
|
||||
- M3 — heartbeat watchdog + REST replay on reconnect.
|
||||
- M4 — polish (keybindings, theme, tmux layout doc).
|
||||
|
||||
Picking the work back up: read [`NEXT_STEPS.md`](./NEXT_STEPS.md) first.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- `uv`
|
||||
- A running `opencode serve` (default `127.0.0.1:4096`)
|
||||
|
||||
## Quickstart
|
||||
|
||||
```sh
|
||||
cd ~/Documents/obsidian/localgenai/oc-tree
|
||||
uv sync
|
||||
uv run oc-tree-probe
|
||||
```
|
||||
|
||||
Drive opencode in another terminal. Probe writes JSONL frames to
|
||||
`/tmp/oc-tree-probe.jsonl` and a live counter to stdout. Ctrl-C to stop;
|
||||
event-type counts print on exit.
|
||||
|
||||
### M0 verification queries
|
||||
|
||||
After driving a session that includes a Task subagent, a permission
|
||||
prompt, and a tool call:
|
||||
|
||||
```sh
|
||||
# 1. Does session.created.info.parentID populate for subagents?
|
||||
jq -r 'select(.type=="session.created") | .raw.properties.info.parentID' \
|
||||
/tmp/oc-tree-probe.jsonl
|
||||
|
||||
# 2. Does message.part.updated carry full parts or deltas?
|
||||
jq -c 'select(.type=="message.part.updated") | .raw.properties.part' \
|
||||
/tmp/oc-tree-probe.jsonl | head
|
||||
|
||||
# 3. Which permission.* events actually fire?
|
||||
jq -r '.type' /tmp/oc-tree-probe.jsonl | grep -i permission | sort -u
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Default |
|
||||
| --------------------------- | ------------------------ |
|
||||
| `OPENCODE_URL` | `http://127.0.0.1:4096` |
|
||||
| `OPENCODE_SERVER_USERNAME` | `opencode` (if pw set) |
|
||||
| `OPENCODE_SERVER_PASSWORD` | _(unset → no auth)_ |
|
||||
|
||||
## References
|
||||
|
||||
- [opencode server docs](https://opencode.ai/docs/server/)
|
||||
- [sst/opencode#7451](https://github.com/sst/opencode/issues/7451) — no per-session SSE endpoint
|
||||
- [sst/opencode#6573](https://github.com/sst/opencode/issues/6573) — Task subagent over `opencode serve`
|
||||
- [sst/opencode#11424](https://github.com/sst/opencode/issues/11424) — replayed message.part.updated frames
|
||||
- [sst/opencode#15149](https://github.com/sst/opencode/issues/15149) — SSE disconnect leaves server hung
|
||||
21
oc-tree/pyproject.toml
Normal file
21
oc-tree/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "oc-tree"
|
||||
version = "0.0.1"
|
||||
description = "Live tree-view sidecar for opencode SSE events"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"textual>=0.80",
|
||||
"httpx>=0.27",
|
||||
"httpx-sse>=0.4",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
oc-tree = "oc_tree.__main__:main"
|
||||
oc-tree-probe = "oc_tree.probe:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/oc_tree"]
|
||||
0
oc-tree/src/oc_tree/__init__.py
Normal file
0
oc-tree/src/oc_tree/__init__.py
Normal file
20
oc-tree/src/oc_tree/__main__.py
Normal file
20
oc-tree/src/oc_tree/__main__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Entry point. M0: stub that points users at the probe.
|
||||
|
||||
The textual app lands in M1+. Until then, this command exists so the
|
||||
script registration in pyproject.toml resolves.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(
|
||||
"oc-tree TUI ships in M1.\n"
|
||||
"For now, run the schema probe:\n"
|
||||
" uv run oc-tree-probe\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
93
oc-tree/src/oc_tree/client.py
Normal file
93
oc-tree/src/oc_tree/client.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Thin async client for opencode's HTTP + SSE API.
|
||||
|
||||
Talks to `opencode serve` (default 127.0.0.1:4096). Auth is off unless
|
||||
OPENCODE_SERVER_PASSWORD is set, matching upstream defaults.
|
||||
|
||||
The single SSE endpoint is `GET /event`; per-session streams don't exist
|
||||
(sst/opencode#7451), so callers filter by sessionID client-side.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from httpx_sse import aconnect_sse
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Event:
|
||||
type: str
|
||||
properties: dict[str, Any]
|
||||
raw: dict[str, Any]
|
||||
|
||||
|
||||
def _auth_header() -> dict[str, str]:
|
||||
pw = os.environ.get("OPENCODE_SERVER_PASSWORD", "")
|
||||
if not pw:
|
||||
return {}
|
||||
user = os.environ.get("OPENCODE_SERVER_USERNAME", "opencode")
|
||||
token = base64.b64encode(f"{user}:{pw}".encode()).decode()
|
||||
return {"Authorization": f"Basic {token}"}
|
||||
|
||||
|
||||
class OpenCodeClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str | None = None,
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
) -> None:
|
||||
self.base_url = (
|
||||
base_url
|
||||
or os.environ.get("OPENCODE_URL")
|
||||
or "http://127.0.0.1:4096"
|
||||
).rstrip("/")
|
||||
# SSE needs no read timeout; REST calls cap at `timeout`.
|
||||
self._sse_timeout = httpx.Timeout(timeout, read=None)
|
||||
self._rest_timeout = httpx.Timeout(timeout)
|
||||
self._headers = _auth_header()
|
||||
|
||||
async def list_sessions(
|
||||
self, *, scope: str = "project", limit: int = 50
|
||||
) -> list[dict[str, Any]]:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers=self._headers,
|
||||
timeout=self._rest_timeout,
|
||||
) as c:
|
||||
r = await c.get("/session", params={"scope": scope, "limit": limit})
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def get_session_messages(self, session_id: str) -> list[dict[str, Any]]:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers=self._headers,
|
||||
timeout=self._rest_timeout,
|
||||
) as c:
|
||||
r = await c.get(f"/session/{session_id}/message")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
async def stream_events(self) -> AsyncIterator[Event]:
|
||||
"""Yield events from /event. Caller handles reconnect."""
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers=self._headers,
|
||||
timeout=self._sse_timeout,
|
||||
) as c:
|
||||
async with aconnect_sse(c, "GET", "/event") as src:
|
||||
async for sse in src.aiter_sse():
|
||||
if not sse.data:
|
||||
continue
|
||||
payload = sse.json()
|
||||
yield Event(
|
||||
type=payload.get("type", ""),
|
||||
properties=payload.get("properties", {}) or {},
|
||||
raw=payload,
|
||||
)
|
||||
100
oc-tree/src/oc_tree/probe.py
Normal file
100
oc-tree/src/oc_tree/probe.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Schema probe: dump raw /event frames to JSONL for inspection.
|
||||
|
||||
Used in M0 to verify three open questions before building the UI:
|
||||
1. Does session.created.info.parentID get populated for Task subagents?
|
||||
(sst/opencode#6573 — may be broken over `opencode serve`.)
|
||||
2. Does message.part.updated carry the full part or a delta?
|
||||
(sst/opencode#11424 — frames sometimes replay full state.)
|
||||
3. What permission.* event names actually fire? (Undocumented.)
|
||||
|
||||
Run, drive opencode through a session that includes a Task-tool
|
||||
subagent + a permission prompt + at least one tool call, then grep the
|
||||
output JSONL for `parentID`, `permission`, and successive part frames.
|
||||
|
||||
Usage:
|
||||
uv run oc-tree-probe # writes /tmp/oc-tree-probe.jsonl
|
||||
uv run oc-tree-probe --out file.jsonl
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from .client import OpenCodeClient
|
||||
|
||||
|
||||
async def _run(out_path: Path) -> int:
|
||||
client = OpenCodeClient()
|
||||
counts: Counter[str] = Counter()
|
||||
print(f"oc-tree probe → {out_path}")
|
||||
print(f" base_url={client.base_url}")
|
||||
print(" drive opencode now; ctrl-c to stop and print summary\n")
|
||||
|
||||
stop = asyncio.Event()
|
||||
loop = asyncio.get_running_loop()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
loop.add_signal_handler(sig, stop.set)
|
||||
|
||||
with out_path.open("a", encoding="utf-8") as f:
|
||||
try:
|
||||
stream = client.stream_events()
|
||||
stream_task = asyncio.create_task(_consume(stream, f, counts))
|
||||
stop_task = asyncio.create_task(stop.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
{stream_task, stop_task},
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for t in pending:
|
||||
t.cancel()
|
||||
for t in done:
|
||||
exc = t.exception()
|
||||
if exc and not isinstance(exc, asyncio.CancelledError):
|
||||
print(f"\nstream error: {exc!r}", file=sys.stderr)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
print("\n--- event counts ---")
|
||||
for t, n in counts.most_common():
|
||||
print(f" {n:>5} {t}")
|
||||
return 0
|
||||
|
||||
|
||||
async def _consume(stream, f, counts: Counter[str]) -> None:
|
||||
async for event in stream:
|
||||
counts[event.type] += 1
|
||||
line = json.dumps(
|
||||
{
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"type": event.type,
|
||||
"raw": event.raw,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
f.write(line + "\n")
|
||||
f.flush()
|
||||
# Live tick so the operator knows it's working.
|
||||
sys.stdout.write(f"\r{sum(counts.values()):>6} events last={event.type:<40}")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="opencode SSE schema probe")
|
||||
ap.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=Path("/tmp/oc-tree-probe.jsonl"),
|
||||
help="output JSONL file (appended)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
return asyncio.run(_run(args.out))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
0
oc-tree/src/oc_tree/widgets/__init__.py
Normal file
0
oc-tree/src/oc_tree/widgets/__init__.py
Normal file
212
oc-tree/uv.lock
generated
Normal file
212
oc-tree/uv.lock
generated
Normal file
@@ -0,0 +1,212 @@
|
||||
version = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify-it-py"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "uc-micro-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
linkify = [
|
||||
{ name = "linkify-it-py" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/3d/e0e8d9d1cee04f758120915e2b2a3a07eb41f8cf4654b4734788a522bcd1/mdit_py_plugins-0.6.0.tar.gz", hash = "sha256:2436f14a7295837ac9228a36feeabda867c4abc488c8d019ad5c0bda88eee040", size = 56025 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl", hash = "sha256:f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90", size = 66655 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oc-tree"
|
||||
version = "0.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "textual" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27" },
|
||||
{ name = "httpx-sse", specifier = ">=0.4" },
|
||||
{ name = "textual", specifier = ">=0.80" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "8.2.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||
{ name = "mdit-py-plugins" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/1e/1eedc5bac184d00aaa5f9a99095f7e266af3ec46fa926c1051be5d358da1/textual-8.2.5.tar.gz", hash = "sha256:6c894e65a879dadb4f6cf46ddcfedb0173ff7e0cb1fe605ff7b357a597bdbc90", size = 1851596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/01/c4555f9c8a692ff83d84930150540f743ce94c89234f9e9a15ff4baba3a8/textual-8.2.5-py3-none-any.whl", hash = "sha256:247d2aa2faf222749c321f88a736247f37ee2c023604079c7490bfacddfcd4b2", size = 727050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uc-micro-py"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383 },
|
||||
]
|
||||
Reference in New Issue
Block a user