docs: settings-menu design spec + implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
||||
# Settings Menu — Design
|
||||
|
||||
**Date:** 2026-06-28
|
||||
**Status:** Approved (design), pending implementation plan
|
||||
**Layers:** frontend, backend (harness involved only as data source)
|
||||
|
||||
## Problem
|
||||
|
||||
When creating a new question, the `NewSessionDialog` form currently asks the user
|
||||
for four parameters inline: **workspace**, **model** (modello), **provider**, and
|
||||
**thinking**. Model/provider are free-text, thinking is free-text, and these choices
|
||||
are re-entered on every new question.
|
||||
|
||||
These four parameters should instead be **global settings**, edited from a dedicated
|
||||
**"Settings"** menu item placed under "Nuova domanda" in the sidebar. The "Nuova
|
||||
domanda" form is reduced to just the question text and uses the current settings.
|
||||
|
||||
The settings must be populated from real sources:
|
||||
|
||||
1. **Workspace** — from the workspace YAML files where workspaces are defined.
|
||||
2. **Model & provider** — chosen among those actually available to Pi, via a real
|
||||
query to Pi (not free text).
|
||||
3. **Thinking** — fixed list: `low`, `medium`, `high`.
|
||||
|
||||
## Decisions (from brainstorming)
|
||||
|
||||
- **Settings scope:** GLOBAL. The "Nuova domanda" form contains only the question
|
||||
text and uses the current settings. There is no per-question override.
|
||||
- **Persistence:** a JSON file on the backend (survives restarts, backend can apply
|
||||
it as defaults).
|
||||
- **Workspace display name:** filename-derived (no new YAML field). YAGNI.
|
||||
- **Settings file location:** `backend/data/settings.json`, path overridable via the
|
||||
`SETTINGS_FILE` env var, gitignored.
|
||||
|
||||
## Sources of truth
|
||||
|
||||
| Setting | Source |
|
||||
|-----------|------------------------------------------------------------------------|
|
||||
| Workspace | `harness/workspaces/*.yaml` (already exposed via `GET /workspaces`) |
|
||||
| Provider | Derived from Pi's available models (group by `model.provider`) |
|
||||
| Model | Pi RPC `get_available_models` → `{ models: [{ provider, id, name, reasoning }] }` |
|
||||
| Thinking | Fixed list `low` / `medium` / `high` |
|
||||
|
||||
### Pi model query — confirmed facts
|
||||
|
||||
`pi --mode rpc` accepts the stdin command `get_available_models` and replies:
|
||||
|
||||
```json
|
||||
{ "type": "response", "id": "...", "command": "get_available_models",
|
||||
"models": [ { "provider": "anthropic", "id": "claude-...", "name": "…", "reasoning": true, ... }, … ] }
|
||||
```
|
||||
|
||||
`modelRegistry.getAvailable()` returns **only models with auth configured** (an API
|
||||
key present in the backend's environment), so every returned model is actually
|
||||
usable. `provider` is a field on each model; the provider list is derived by grouping.
|
||||
Pi supports thinking levels `off, minimal, low, medium, high, xhigh`; we expose only
|
||||
`low/medium/high`.
|
||||
|
||||
## Harness layer
|
||||
|
||||
No code change required. The harness remains:
|
||||
- the **source** of workspace YAML files (read by the backend via `GET /workspaces`),
|
||||
- the tool whose `tht session new` already accepts `--provider/--model/--thinking`,
|
||||
- the wrapper around Pi, which answers `get_available_models`.
|
||||
|
||||
## Backend layer (Fastify)
|
||||
|
||||
### Settings store
|
||||
- New module persisting `{ workspace, provider, model, thinking }` to a JSON file
|
||||
(default `backend/data/settings.json`, overridable via `SETTINGS_FILE`).
|
||||
- On read, if the file is absent, return effective defaults: env
|
||||
`PI_PROVIDER/PI_MODEL/PI_THINKING` plus the first workspace from `GET /workspaces`.
|
||||
|
||||
### Endpoints
|
||||
- `GET /settings` → current effective settings `{ workspace, provider, model, thinking }`.
|
||||
- `PUT /settings` → persist new settings. Validates `model` against the available
|
||||
models from Pi; rejects unknown models with a clear error. (If Pi returns no models —
|
||||
unavailable — validation is skipped so settings can still be saved.)
|
||||
- `GET /models` → completes the existing stub: ephemeral Pi spawn + `get_available_models`,
|
||||
returns `{ models: [{ provider, id, name, reasoning }] }`. Result is cached in memory
|
||||
(Pi startup is slow); graceful fallback to `{ models: [] }` when Pi is unavailable.
|
||||
- `GET /workspaces` → unchanged.
|
||||
|
||||
### Ephemeral model listing
|
||||
- Spawn `pi --mode rpc` with the same cwd/env as session spawns (so API keys resolve),
|
||||
send `get_available_models`, read the response, kill the child.
|
||||
- Cache the result in memory for the process lifetime; expose an implicit refresh by
|
||||
letting the cache be re-queried (a query param or a short TTL — implementation detail
|
||||
for the plan). Keep the existing injectable seam (`listModels` dep) so tests stub it.
|
||||
|
||||
### `POST /sessions`
|
||||
- No longer requires `workspace/model/provider/thinking` from the frontend.
|
||||
- Reads the settings file and applies them:
|
||||
- `workspace` → selects the `-c <config>` for `tht session new`,
|
||||
- `provider`/`model` → `set_model`,
|
||||
- `thinking` → `set_thinking_level`.
|
||||
- Request body is reduced to `{ question }` (plus optional `name`). Any settings-like
|
||||
fields in the body are ignored (no per-question override).
|
||||
|
||||
## Frontend layer (React + base-ui + Tailwind + React Query)
|
||||
|
||||
### NewSessionDialog
|
||||
- Remove the workspace / model / thinking / provider fields.
|
||||
- Keep only the question textarea. Submit sends `{ question }` to `POST /sessions`.
|
||||
|
||||
### Settings menu + dialog
|
||||
- Add a **"Settings"** button in the `AppShell` sidebar, directly under
|
||||
"Nuova domanda", styled as a secondary (`variant="outline"`) button, matching the
|
||||
existing button conventions.
|
||||
- New `SettingsDialog` (same Dialog/base-ui pattern as `NewSessionDialog`) with four
|
||||
controls:
|
||||
- **Workspace** — `<select>` from `GET /workspaces`.
|
||||
- **Provider** — `<select>` derived from the available models; changing it filters
|
||||
the model list.
|
||||
- **Model** — `<select>` filtered by the chosen provider (from `GET /models`).
|
||||
- **Thinking** — `<select>` fixed `low/medium/high`.
|
||||
- Loads current values from `GET /settings`; saves with `PUT /settings`.
|
||||
- **Degradation:** if `GET /models` returns an empty list (Pi unavailable / no keys),
|
||||
show a notice and fall back to free-text inputs for provider and model so settings
|
||||
remain editable.
|
||||
|
||||
### API client
|
||||
- `getSettings()` / `putSettings(s)` for `/settings`.
|
||||
- `listModels()` updated to the `{ models: [{provider, id, name, reasoning}] }` shape.
|
||||
- `createSession` simplified to send only `{ question }` (+ optional `name`).
|
||||
|
||||
## Error handling / edge cases
|
||||
|
||||
- **Pi unreachable or zero models:** `/models` → `[]`; `SettingsDialog` warns and
|
||||
degrades to text inputs; nothing blocks.
|
||||
- **Stale settings (model no longer available):** `PUT /settings` rejects unknown
|
||||
models when a model list is available; at session-creation time stored values are
|
||||
passed through to Pi as-is (Pi reports if invalid).
|
||||
- **First run (no settings file):** `GET /settings` returns env/first-workspace
|
||||
defaults; the dialog is usable immediately.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- No new field in the workspace YAML schema.
|
||||
- No per-question override of settings.
|
||||
- No multi-user / per-user settings (single shared settings file).
|
||||
Reference in New Issue
Block a user