Metadata-Version: 2.4
Name: attune-gui
Version: 0.5.3
Summary: Local dashboard for attune-rag / attune-help / attune-author. Server-rendered Jinja2 UI — ships clean via PyPI with no npm step.
Project-URL: Homepage, https://github.com/Smart-AI-Memory/attune-gui
Project-URL: Repository, https://github.com/Smart-AI-Memory/attune-gui
Project-URL: Issues, https://github.com/Smart-AI-Memory/attune-gui/issues
Author-email: Patrick Roebuck <admin@smartaimemory.com>
License: Apache-2.0
License-File: LICENSE
Keywords: attune,developer-tools,documentation,gui,rag
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: attune-author[ai]<0.6,>=0.5.0
Requires-Dist: attune-help<1.0,>=0.10.0
Requires-Dist: attune-rag<0.2,>=0.1.12
Requires-Dist: fastapi<1.0,>=0.110
Requires-Dist: jinja2<4.0,>=3.1
Requires-Dist: markdown<4.0,>=3.5
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: python-frontmatter<2.0,>=1.1
Requires-Dist: structlog<26.0,>=24.0
Requires-Dist: uvicorn[standard]<1.0,>=0.27
Provides-Extra: dev
Requires-Dist: build<2.0,>=1.0; extra == 'dev'
Requires-Dist: httpx<1.0,>=0.27; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
Requires-Dist: pytest<10.0,>=8.0; extra == 'dev'
Requires-Dist: ruff<1.0,>=0.5; extra == 'dev'
Requires-Dist: twine<7.0,>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# attune-gui

Local dashboard for the `attune-*` documentation family
(`attune-rag`, `attune-help`, `attune-author`). Server-rendered Jinja2
UI ("Cowork dashboard") backed by a FastAPI sidecar — ships clean via
PyPI with no `npm` step required to run.

## What it does

Sidebar nav with seven pages, each consuming the existing JSON API:

| Page | What it shows |
|------|---------------|
| **Health** | Cross-layer health (rag/help/author/gui versions) + corpus snapshot |
| **Templates** | Markdown templates with mtime staleness, tags, and a manual-pin toggle |
| **Specs** | Feature specs in `specs/` with phase + status badges. **+ New spec** bootstraps from `TEMPLATE.md`; **+ Design / + Tasks** inline; status dropdown in Preview |
| **Summaries** | Inline-editable `summaries.json` with overwrite warning |
| **Living Docs** | Workspace editor, scan trigger, document health, review queue, RAG quality bars |
| **Commands** | Run any registered command from a card grid (RAG queries, regen, maintain, …) |
| **Jobs** | Job history with per-feature progress, last-output column, Cancel button, auto-refresh |

Click any spec or template to open the **Preview / Edit** panel — server-side
Markdown rendering plus a raw `<textarea>` for editing.

## Template editor (`/editor`)

A first-class CodeMirror 6 editor for attune-help-style markdown templates.
Triggered by `attune-author edit <path>` or by navigating to
`/editor?corpus=<id>&path=<rel>`. Reads from / writes back to any registered
corpus on disk, with all retrieval/lint/refactor smarts coming from
[`attune-rag`](https://pypi.org/project/attune-rag/)'s `editor` toolkit.

Developer features:

| Feature | What it does |
|---------|---------------|
| **Schema-driven frontmatter form** | Reads `attune_rag.editor.load_schema()` at request time and renders typed inputs (select / chip-input / textarea / text). Unknown frontmatter keys are preserved verbatim. Raw-YAML toggle round-trips byte-for-byte. |
| **CodeMirror 6 + Lezer extension** | Composes the standard `@codemirror/lang-markdown` with a custom Lezer extension for YAML frontmatter delimiters, `## Depth N` markers, and `[[alias]]` refs (excluding fenced code, supporting `\[[` escape). |
| **Server-side lint** | 300 ms debounced POST to `/api/corpus/<id>/lint`. Diagnostics paint as squiggles in the editor + a clickable strip at the bottom. Local fast-path skips the round-trip for YAML parse errors. |
| **Tag + alias autocomplete** | Context-aware completions inside `tags:` / `aliases:` frontmatter fields and `[[…]]` body refs. Per-prefix LRU cache invalidates on WS file-change events. |
| **Per-hunk save modal** | `/api/corpus/<id>/template/diff` returns stable hunk ids; the modal shows each hunk with a checkbox and runs a *projected-state* lint so a partial save can't write known-broken frontmatter. Atomic save via `/template/save` (409 on `base_hash` mismatch → conflict mode). |
| **3-way merge conflict mode** | A WebSocket at `/ws/corpus/<id>?path=<rel>` pushes `file_changed` events from `watchfiles`. On conflict (or 409), a banner offers Reload / Keep / Resolve. Resolve uses [`node-diff3`](https://github.com/bhousel/node-diff3) for per-region accept-disk / accept-editor / keep-both. |
| **Cross-corpus rename refactor** | Right-click any tag/alias chip → "Rename …". `/api/corpus/<id>/refactor/rename/preview` returns a multi-file diff; apply is atomic across files (per-file tempfile + sequential rename + drift-detection rollback). 409 with `owning_path` on alias collisions. |
| **Corpus switcher** | Top-bar dropdown lists registered corpora (`~/.attune/corpora.json`). Search input materializes above 10 corpora. "+ Add corpus…" registers a new root via `/api/corpus/register`. Switching with unsaved edits prompts Save / Discard / Cancel. |
| **Generated-corpus advisory** | Persistent, non-dismissible banner when the active corpus has `kind: "generated"` — flags that edits will be overwritten on the next `attune-author maintain`. |
| **Read-only on duplicate session** | A second tab opening the same `(corpus, path)` receives a `duplicate_session` WS message and goes read-only with a banner — first tab keeps full control. |
| **Keyboard shortcuts** | `⌘/Ctrl-S` opens the save modal. `beforeunload` warns on unsaved edits. |
| **Pre-bundled, no Node at install** | `editor-frontend/` is Vite + TypeScript; `make build-editor` produces a hashed-filename bundle into `sidecar/attune_gui/static/editor/` that's checked into the repo. PyPI consumers don't need `npm`. |

### Editor frontend dev loop

```bash
# In one shell — sidecar with auto-reload:
uv run attune-gui --port 8765 --reload

# In another — vitest watch (92 unit tests, ~2s):
cd editor-frontend && npm run test --watch

# Rebuild the bundle (deterministic; output committed):
make build-editor

# Run the Playwright e2e suite against a fresh sidecar
# (auto-spawned by playwright.config.ts):
cd editor-frontend && npm run e2e
```

The editor bundle is ~210 KB gzipped (budget: 600 KB). Schema and Lezer
grammar parse fixtures live in `editor-frontend/src/grammar/`; merge
correctness in `three-way-merge.test.ts`. The Playwright suite under
`editor-frontend/e2e/` exercises the end-to-end flows (open→edit→save,
conflict resolve via WS-pushed `file_changed`, rename refactor
preview+apply, corpus switcher with unsaved-edits guard, save-modal
controls, keyboard shortcuts + advisories) — it spins up a real
sidecar with an isolated `ATTUNE_CORPORA_REGISTRY` so your dev
registry isn't touched.

### Endpoint summary

| Method | Path | Purpose |
|--------|------|---------|
| GET    | `/editor?corpus=<id>&path=<rel>` | Editor shell (Jinja) |
| GET    | `/api/editor/template-schema` | Frontmatter JSON Schema |
| GET    | `/api/corpus` | List registered corpora + active id |
| POST   | `/api/corpus/active` | Switch active corpus |
| POST   | `/api/corpus/register` | Add a new corpus root |
| POST   | `/api/corpus/resolve` | Map an absolute path → `(corpus_id, rel_path)` |
| GET    | `/api/corpus/<id>/template?path=<rel>` | Read template + base hash + mtime |
| POST   | `/api/corpus/<id>/template/diff` | Compute unified-diff hunks vs disk |
| POST   | `/api/corpus/<id>/template/save` | Atomic save (`base_hash` 409-guarded) |
| POST   | `/api/corpus/<id>/lint` | Lint a template body |
| GET    | `/api/corpus/<id>/autocomplete?kind=tag\|alias&prefix=…` | Autocomplete |
| POST   | `/api/corpus/<id>/refactor/rename/preview` | Multi-file rename plan |
| POST   | `/api/corpus/<id>/refactor/rename/apply` | Atomic rename across files |
| WS     | `/ws/corpus/<id>?path=<rel>` | `file_changed` + `duplicate_session` push |

> Looking for AI dev workflows (code review, security audits, refactor
> planning, multi-agent orchestration)? Those live in
> [`attune-ai`](https://pypi.org/project/attune-ai/) — a separate
> product. attune-gui is deliberately scoped to the documentation lifecycle.

## Quickstart

```bash
pip install attune-gui
attune-gui
# Or pick a specific port:
attune-gui --port 8765
```

The sidecar binds to `127.0.0.1`, prints `SIDECAR_URL=…`, and serves the
new dashboard at `/`. Use `--open` to auto-open your browser.

## Configuration

### `.env` auto-loading

The sidecar loads `KEY=value` lines from the first `.env` it finds, in this order:

1. `./.env` (current working directory)
2. `<repo-root>/.env` (the attune-gui checkout root)
3. `~/.attune-gui/.env`
4. `~/.attune/.env`

Existing real env values are preserved; empty/whitespace-only values are
treated as unset and overwritten. Common keys:

```
ANTHROPIC_API_KEY=sk-ant-…   # required for author.regen / author.maintain
ATTUNE_SPECS_ROOT=/path/to/your/repo/specs
ATTUNE_WORKSPACE=/path/to/your/project
```

### `~/.attune-gui/config.json`

A single typed config file holds the keys below. Each key has a matching
environment variable that overrides the file at runtime — useful for CI
or one-off runs. Precedence: **env > file > default**.

| Key | Env var | Purpose |
|-----|---------|---------|
| `workspace` | `ATTUNE_WORKSPACE` | Project the sidecar watches (Living Docs, templates) |
| `corpora_registry` | `ATTUNE_CORPORA_REGISTRY` | Path to corpora registry (default: `~/.attune/corpora.json`) |
| `specs_root` | `ATTUNE_SPECS_ROOT` | Where the **Specs** page reads from (default: `<workspace>/specs/`, then walks up from cwd) |

Manage the file from the CLI — no hand-editing JSON:

```bash
attune-gui config list                               # show resolved values + source
attune-gui config get workspace                      # print one value
attune-gui config set workspace /path/to/project     # persist a value
attune-gui config unset specs_root                   # remove a key
```

`config set workspace` validates that the path is a real directory; the
others trust you. Workspace can also be set via **Living Docs →
Workspace** in the UI — both surfaces write to the same file.

## Development

```bash
git clone https://github.com/Smart-AI-Memory/attune-gui
cd attune-gui
uv sync
uv run attune-gui --port 8765 --reload
```

Templates auto-reload — edit anything under `sidecar/attune_gui/templates/`
and refresh the browser. Python code changes reload automatically with
`--reload`.

### Tests

```bash
uv run pytest                # 124 tests, ~2s
uv run ruff check .          # lint
```

## Architecture

```
┌──────────────────────────────────────┐
│  Cowork dashboard (Jinja2)  /        │
└──────────────────┬───────────────────┘
                   │  /api/*
┌──────────────────▼───────────────────┐
│  FastAPI sidecar — 127.0.0.1         │
│  ├─ routes/system, rag, help, …      │
│  ├─ routes/cowork_health             │
│  ├─ routes/cowork_specs              │
│  ├─ routes/cowork_templates          │
│  ├─ routes/cowork_files              │
│  └─ routes/cowork_pages  (HTML)      │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│  attune-rag · attune-help            │
│  attune-author[ai]                   │
└──────────────────────────────────────┘
```

### Frontend boundary

Two rendering surfaces, one rule:

- **Template editor at `/editor`** is a Vite + TypeScript SPA
  (CodeMirror 6, WebSocket conflict mode, per-hunk diff). Source in
  `editor-frontend/`, pre-bundled into `sidecar/attune_gui/static/editor/`
  and committed so PyPI consumers do not need Node.
- **Everything else** (Health, Templates, Specs, Summaries, Living Docs,
  Commands, Jobs) is server-rendered Jinja2 with light vanilla-JS for
  inline actions and polling.

**New UI defaults to Jinja.** Reach for the SPA only when the surface
needs editor-grade interactivity — rich text editing, real-time conflict
resolution, multi-file refactor previews. Inline actions, polling, and
form-driven dashboards stay server-rendered.

## Security notes

This is a **single-user, local-only** app. Not designed for multi-user
deployment, not hardened against a motivated attacker on the same machine.

- Binds **only** to `127.0.0.1` — not reachable from other machines
- An `Origin` header guard rejects browser requests from non-localhost origins
- Mutating endpoints require the `X-Attune-Client` header to match a
  per-process token (served from `/api/session/token`)
- File API enforces a path-traversal guard against three named roots
  (`templates`, `specs`, `summaries`); writes outside those roots return 400

## Related packages

- [`attune-rag`](https://pypi.org/project/attune-rag/) — RAG pipeline
- [`attune-help`](https://pypi.org/project/attune-help/) — help runtime
- [`attune-author`](https://pypi.org/project/attune-author/) — doc authoring
- [`attune-gui-plugin`](https://github.com/Smart-AI-Memory/attune-gui-plugin) — Claude Code plugin that launches the dashboard inside Cowork's preview pane
- [`attune-ai`](https://pypi.org/project/attune-ai/) — separate AI dev workflow product (not used by attune-gui)

## License

Apache-2.0
