An edge that is not drawn, and a panel that stopped eating the site

`hx-get=""` is not "fetch nothing". htmx looks for the attribute, not for a
value, so the empty one the canvas rendered before a chat existed was a real
request for the empty path -- which the browser resolves against the current
document. Opening the canvas on the new-chat screen fetched the new-chat screen
and swapped the whole site into the panel. The attribute is omitted now, and a
test refuses an empty verb anywhere on the page.

Which panels can exist is the server's answer; which are offered is the
browser's. Both need an agent chat on a chosen connection, and before a chat
exists those are controls in the composer -- so answering with the first profile
offered a terminal on an ordinary chat with nothing selected. They follow
`lembas:agent-target` now, and an open panel whose target goes away is closed
rather than left showing one machine under another's name.

`.tabs__body` is only sometimes the scroller: true where the tabs are a bounded
flex child, false under the admin layout, where the page scrolls instead. So
setting its scrollTop on every tab change had never once run on /admin/prompts,
silently, while the reader was dragged to the bottom of a document that had just
got shorter. The rule names the position now, and the handler finds the
container that actually scrolls.

The two top borders come off. They were what made the misalignment at the bottom
of the shell visible; `--footer-height` stays, because two ends at different
heights are visible without a line to prove it. The top of the shell keeps its
line -- there, everything is `--header-height` and aligns by construction.

And one version. pyproject carried its own copy and had drifted three minors
from the one everything actually reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-06 09:42:06 +02:00
parent 00ce04addf
commit fa8c8ab5e8
18 changed files with 1423 additions and 984 deletions
+120 -12
View File
@@ -4,12 +4,15 @@ Where the project is, what is deliberately not built yet, and the decisions
that would be expensive to revisit. Kept current as work lands; the detail of
*how* things work lives in [`CLAUDE.md`](CLAUDE.md).
**Status:** usable daily. Streaming chat, attachments, reasoning, tool calling
with web search, custom HTTP tools and MCP servers, agent chats that work on a
machine over SSH, a knowledge library, notes, memory and skills, speech in and
out, image generation over ComfyUI, users and groups, model administration,
installable as an app, reports, messages, and scheduled work that runs on its
own. 1824 tests, `ruff` clean.
**Status:** usable daily, and closing on 1.0.0. Streaming chat, attachments,
reasoning, tool calling with web search, custom HTTP tools and MCP servers,
agent chats that work on a machine over SSH, a knowledge library, notes, memory
and skills, speech in and out, image generation over ComfyUI, users and groups,
model administration, installable as an app, reports, messages, and scheduled
work that runs on its own. 1824 tests, `ruff` clean.
What remains before the first stable release is written out below, in phases,
under [The road to 1.0.0](#the-road-to-100).
---
@@ -369,19 +372,124 @@ be a different project, not a refactor.
---
## Not built yet
## The road to 1.0.0
In the order they are likely to be worth doing.
What is left is not another large feature. It is four kinds of work: gaps that
read as bugs, features still owed, two structural jobs, and making this
installable and updatable by somebody who is not its author.
Each phase ends the same way, and that is a requirement rather than a habit:
tests green, `ruff` clean, `__version__` bumped (the service worker cache is
keyed on it, so a release without a bump serves stale JavaScript), committed,
pushed, and `deploy/update.sh` run — so the next phase starts from something
seen working.
### Phase 0 — the known bugs, and the CSS (`0.8.x`)
- [ ] **One version, one homepage.** `pyproject.toml` reads `__version__`
instead of carrying its own copy of it, which had drifted three minors
- [ ] **Canvas and Terminal appear only where they can work.** `hx-get=""` is an
attribute htmx *finds*, so an empty one fetches the current document and
swaps the whole site into the canvas panel. The buttons follow the
composer's kind toggle and its connection, which only the browser knows
- [ ] **The two top borders come off.** The sidebar footer and the composer sat
either side of one vertical edge and were held to the same height so their
borders would meet. Content scrolling under an edge that is not drawn is
better than an edge that has to be aligned
- [ ] **One scroll container per screen.** `.tabs` assumes it is a flex child of
`.main`; under the admin layout it is not, so `.tabs__body` never scrolls,
the outer container does, and switching to a shorter panel drops the
reader at the bottom of the page
- [ ] Sidebar scroll no longer chains to the document
### Phase 1 — the scheduling tools (`0.9.0`)
- [ ] **A model can schedule.** There was no tool for it — the seam was left
(`Schedule.origin` has defined `ORIGIN_MODEL` with no writer since
scheduling landed) and the tool was never built, so a model asked to
"remind me every Monday" wrote a note and said it had. `schedule_create`,
`schedule_list`, `schedule_update` and `schedule_cancel` over the same
`rule.validate` the form and the compile already share
- [ ] Guidance saying which target a run should reach, and that anything which
happens later or repeatedly is a schedule rather than a note
### Phase 2 — image generation admin (`0.9.1`)
- [ ] **Defaults an administrator can set** — steps, cfg, size, sampler,
scheduler, denoise, negative, batch. There were none: one hardcoded set
from the SD1.5 era, and prose in a box as the only way to change it
- [ ] The right control for each: samplers and schedulers as selects, from the
lists ComfyUI has been discovering and nothing has been reading;
checkpoints picked rather than typed; sizes as numbers with presets
- [ ] A legend on the workflow editor saying what each placeholder fills and
what type it lands as
### Phase 3 — subagents (`0.9.2`)
- [ ] **A model can delegate.** A bounded, unattended agent with the parent's
connection, directory, model and effort, whose findings come back as the
tool result. Built on the mechanism scheduled runs already use, so it gets
tools, rounds and budgets rather than a second loop
- [ ] **Safe by resolution, not by instruction** — no `ask_user`, no recursion,
read-only tools by default, and in an agent chat a mode that cannot run
what it was not given
- [ ] Guidance for the two uses that differ: fanning out across a research
question, and reading a codebase
### Phase 4 — rebranding and customization (`0.9.3`)
- [ ] **An instance can be somebody else's.** Name, logo, favicon and PWA icons,
a tagline, and the Middle-earth strings as editable data — defaults in
code and overrides in the database, so a later release still improves the
wording nobody changed
- [ ] Global CSS overrides, and a custom theme defined as a set of tokens rather
than a stylesheet, since no component hard-codes a colour
### Phase 5 — extraction, embeddings and hybrid search (`0.9.4`)
- [ ] **Extraction has settings** — upload size, image edge, PDF pages,
extracted characters, orphan age, which extensions count as text
- [ ] **A dedicated embedding model**, chosen from the models flagged for it
- [ ] **Search becomes hybrid** — FTS5 and vector recall fused, behind the one
call the retrieval service already is. No model chosen means exactly the
keyword search there is today
### Phase 6 — permissions, quotas and sharing (`0.9.5`)
- [ ] **"What can this user actually do?"** answered on screen, from the
resolution that already computes it
- [ ] Membership edited from one side; a searchable, paginated user list
- [ ] Reading and writing split within a gate where the difference matters
- [ ] **Quotas on a group**, resolved by maximum — the union rule applied to
numbers — and enforced where the existing budgets are
- [ ] Deleting a group or a user forgets its grants, which it never did
- [ ] Sharing as its own action with a search box, a shared-with-me filter, and
reports shareable. Sharing stays read-only
### Phase 7 — packaging and updating (`0.9.6`)
- [ ] **Docker**, with the data on a volume and a TLS proxy expected in front
- [ ] **An LXC bootstrap** that runs the existing installer in a container
- [ ] **Updating without a shell** — a page that says what is running, what is
available and what changed, and a button answered by an opt-in systemd
helper, because the service user cannot restart itself and should not
### Phase 8 — audit and finalization (`0.9.7` … `0.9.9`)
- [ ] Security review over the whole accumulated diff
- [ ] A sweep for the failure this codebase keeps cataloguing: a control that
looks like it works — a verb against a route that does not serve it, a
trigger bound where the event does not go
- [ ] Every harness fragment read as a model would read it
- [ ] Focus, contrast and narrow widths across the admin screens
- [ ] Documentation, a fresh install, and an upgrade from an 0.8.x database
### Phase 9 — 1.0.0
- [ ] A commit that changes the version, this file and the README, and tags it
---
## After 1.0.0
### Smaller things
- **OCR** for scanned PDFs
- **Conversation branching** — `Message.parent_id` exists unused; needs a UI for
choosing between versions, which is why rewind truncates for now
- **Chat export** (Markdown, JSON)
- **Semantic search** in the library — the retrieval service is one call, so an
embedding backend can go behind it without touching the tools or the UI
- **Archived chats** — the column exists, nothing surfaces it
- **Per-user quotas**
- **Several workers** — see the first known limit below
- **Writable shares**, which need history and a merge story before they need a
column
---