Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Dashboard

on:
pull_request:
paths:
- 'dashboard/**'
- 'src/serena/resources/dashboard/**'
push:
branches:
- main

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: dashboard
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: dashboard/package-lock.json
- run: npm ci
- run: npm run check
- run: npm test
- run: npm run lint
- run: npm run build
- name: Fail if committed build output is stale
run: git diff --exit-code -- ../src/serena/resources/dashboard
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ lib/
!test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart
!test/resources/repos/svelte/test_repo/src/lib/
!test/resources/repos/svelte/test_repo/src/lib/**
!dashboard/src/lib/
!dashboard/src/lib/**
lib64/
parts/
sdist/
Expand Down Expand Up @@ -276,3 +278,6 @@ zz-misc/
vue-implementation/

news/news.json

# Playwright MCP session artifacts (screenshots, console logs, page snapshots)
.playwright-mcp/
2 changes: 1 addition & 1 deletion .serena/memories/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Serena is an MCP-based "IDE for coding agents": semantic code retrieval/editing/
- `config/` — `serena_config.py`, `context_mode.py`, `client_setup.py`
- `resources/config/contexts/*.yml`, `resources/config/modes/*.yml` — context/mode definitions
- `code_editor.py`, `symbol.py`, `ls_manager.py` — symbolic editing / LS lifecycle
- `dashboard.py`, `gui_log_viewer.py` — web dashboard / log viewer
- `dashboard.py`, `gui_log_viewer.py` — web dashboard backend (frozen API) / log viewer; the Svelte frontend lives in `dashboard/` — see `mem:dashboard_frontend`
- `prompt_factory.py` + `generated/generated_prompt_factory.py` — prompts (regenerate with `scripts/gen_prompt_factory.py`)
- `src/solidlsp/` — LSP client framework; per-language servers under `language_servers/`
- `src/interprompt/` — prompt template library (synced from external repo; see `.syncCommitId.*`)
Expand Down
42 changes: 42 additions & 0 deletions .serena/memories/dashboard_frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Dashboard Frontend

Svelte 5 (runes) + TS + Vite SPA in `dashboard/`. Built into
`src/serena/resources/dashboard/` and served by Flask in `src/serena/dashboard.py`
at `/dashboard/`. **`dashboard/CLAUDE.md` is the authoritative ruleset** — read it
before editing anything under `dashboard/`; this memory only records the
hard invariants.

## Hard invariants

- **Backend is a frozen contract.** `dashboard.py` endpoint names, request/response
shapes, ports, and the host-header check must not change from the frontend.
Canonical route list = `API_ROUTES` in `dashboard/vite.config.ts`.
- **Build output is committed & CI-enforced.** After any `dashboard/src` change:
`npm run build` (writes hashed assets to `src/serena/resources/dashboard/`) and
commit the regenerated `index.html` + `assets/`. Stale output fails CI
(`.github/workflows/dashboard.yml`; poe task `poe build-dashboard`).
- Before committing dashboard work: `npm run format` and **stage all changes** — a
partial stage leaves files prettier-dirty and CI's `prettier --check` fails.
- `prebuild` (`scripts/clean-assets.mjs`) clears `assets/` because
`emptyOutDir: false` (the dir also holds icon/logo PNGs).

## Commands (run from `dashboard/`)

`npm run dev` (Vite :5273, proxies API to a Serena server on :24282) ·
`npm run build` · `npm run check` (svelte-check) · `npm test` (Vitest) ·
`npm run lint` / `npm run format`. Dev needs a running Serena MCP server with the
dashboard enabled; logos/icons 404 under dev (backend-served).

## Architecture invariants

- Layered: `lib/api/` (`types.ts` → `endpoints.ts` → `client.ts`, the only `fetch`)
→ `lib/stores/*.svelte.ts` (runes singletons, getter-only) → components. Never
`fetch` from a component.
- Two backend failure channels (non-2xx `ApiError` AND HTTP-200
`{status:'error'}`); normalize every mutation through `runMutation()`.
- `$derived` for computed values; `$effect` only for true side effects (never to
sync state). Reactive collections use `SvelteSet`/`SvelteMap`. Colors via
`var(--token)` from `styles/tokens.css`, never hardcoded. Charts only through
`ChartPanel.svelte`. Snippets, not `<slot>`. No jQuery.
- Each store/lib gets a unit test; each component a render+interaction test.
Vitest + jsdom; chart-mounting tests must mock `chart.js/auto`.
1 change: 1 addition & 0 deletions .serena/memories/tech_stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
- Dev deps: `ruff` (lint+format), `mypy` (strict), `pytest` (+ `pytest-xdist`, `pytest-timeout`, `syrupy` snapshots), `sphinx`/`jupyter-book` for docs.
- Optional extras: `agno` (Agno agent integration), `google` (gemini).
- LSP client core lives under `src/solidlsp/`; one subdir per supported language server under `language_servers/`.
- Dashboard frontend: a separate **Svelte 5 + TypeScript + Vite** project under `dashboard/` (its own npm world). Its build output is committed to `src/serena/resources/dashboard/` and shipped in the wheel; Python-only contributors need no Node.
4 changes: 4 additions & 0 deletions dashboard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.vite
*.local
3 changes: 3 additions & 0 deletions dashboard/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
package-lock.json
1 change: 1 addition & 0 deletions dashboard/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "singleQuote": true, "printWidth": 100, "plugins": ["prettier-plugin-svelte"] }
163 changes: 163 additions & 0 deletions dashboard/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Serena Dashboard (frontend)

Svelte 5 (runes) + TS + Vite SPA. **Frontend only** — the HTTP API lives in
`../src/serena/dashboard.py` and is a **frozen contract**: never change endpoint
names, request/response shapes, ports, or the host-header check from here. The
canonical list of routes the app calls is `API_ROUTES` in `vite.config.ts`.

## Commands (run from `dashboard/`)

- `npm run dev` — Vite on :5273, proxies API routes to a backend on :24282
- `npm run build` — hashed assets into `../src/serena/resources/dashboard/`
- `npm run check` — `svelte-check` · `npm test` — Vitest · `npm run lint` / `format`

For `npm run dev` start any Serena MCP server with the dashboard enabled first
(logos/icons 404 under dev — they're served by the backend). See `README.md` for
the human quick-start.

## The build-output contract (CI enforces)

After **any** source change run `npm run build` and commit the regenerated
`../src/serena/resources/dashboard/` (`index.html` + `assets/`) — it ships in the
wheel and CI fails the PR if it's stale. Before committing run `npm run format`
and **stage all changes** (a partial stage can leave other files prettier-dirty
and fail CI's `prettier --check`). `prebuild` (`scripts/clean-assets.mjs`) clears
`assets/` first because `emptyOutDir: false` (the dir also holds icon/logo PNGs).

## Architecture rules

- Components are small, single-purpose: a typed `interface Props` +
`let {...} = $props()`, props in / events out (`on*` callback props, **not**
`createEventDispatcher`), scoped CSS. Compose `common/` primitives (`Button`,
`Card`, `Collapsible`, `Combobox`, `FilterDropdown`, `Icon`, `Modal`,
`Popover`, `Spinner`) — don't re-implement their markup.
- **Derive, don't sync.** Compute reactive values with `$derived`; reserve
`$effect` for true side effects (DOM, Chart.js, subscriptions, autoscroll).
Never use `$effect` to copy one piece of state into another — that's a
`$derived`.
- Icons: import a `@lucide/svelte` component and pass it to `Icon`
(`<Icon icon={Foo} label="…" />`); the `label` toggles `role="img"`/`aria-label`
vs `aria-hidden`, so a11y stays centralized. No raw inline `<svg>`, no bare
lucide component in markup.
- Colors come from `src/styles/tokens.css` (light + `[data-theme='dark']`).
Never hardcode hex; use `var(--token)`.
- Pass markup into a component as **snippets** (`children` typed `Snippet`,
rendered with `{@render children()}`) — never the deprecated `<slot>`.
- Charts go **only** through `src/components/stats/ChartPanel.svelte`.
- Never `fetch` from a component — all network goes through `src/lib/api/`.
- Never reintroduce jQuery.

## Recipes

- **Backend-backed feature:** type in `api/types.ts` → typed fn in
`api/endpoints.ts` (+ add the route to `API_ROUTES` in `vite.config.ts`) →
store under `stores/*.svelte.ts` → component → Vitest test → `npm run build` &
commit output.
- **Modal:** extend `ModalState` (`stores/modal.svelte.ts`) → add a case in
`ModalHost.svelte` → build the component (wrap `ConfirmModal` if confirm-style;
`createModalAction()` if it mutates).
- **Chart:** add a pure `ChartSpec` builder in `charts.ts`, render via
`ChartPanel` — never import `chart.js` elsewhere.
- **Common primitive:** add to `components/common/`, drive variants by props,
expose content as snippets, style with `var(--token)` only.

## State: runes stores (`src/lib/stores/*.svelte.ts`)

`$state` only lives in `.svelte.ts` modules. Each store is a factory returning
**getter-only** accessors plus action methods, exported as a singleton:

```ts
export function createXStore() {
let data = $state<T | null>(null);
return {
get data() {
return data;
},
async poll() {
data = await fetchX();
},
};
}
export const x = createXStore(); // import the singleton; factory exists for tests
```

Never expose the `$state` variable directly — only getters, so reads stay
reactive and writes funnel through methods.

- Reactive `Set`/`Map` use `SvelteSet`/`SvelteMap` from `svelte/reactivity` (see
`expanded` in `code.svelte.ts`) — plain `Set`/`Map` mutations don't trigger
updates.
- Guard overlapping async writes with a plain (non-reactive) **epoch counter**:
bump + snapshot it before the `await`, then drop the result if the snapshot is
stale (see `searchEpoch`/`diagEpoch` in `code.svelte.ts`). Stops a slow earlier
request from clobbering a newer one.

## API layer (`src/lib/api/`)

- `types.ts` — TS mirrors of backend JSON. `endpoints.ts` — one typed fn per
route. `client.ts` — the **only** place that calls `fetch` (`getJson` /
`postJson` / `putJson`, throws `ApiError` on non-2xx).
- **Two failure channels.** The backend signals failure either as a non-2xx
(thrown `ApiError`) _or_ as HTTP 200 with `{ status: 'error', message }`. Wrap
every mutating call in `runMutation(fn)` (`mutation.ts`) — it normalizes both
into `{ ok, message?, data? }`. Don't hand-roll try/catch around endpoints.

## Modals

- One discriminated union `ModalState` in `stores/modal.svelte.ts`; the global
`modal` store has `open(state)` / `close()`. `App.svelte` opens modals;
`ModalHost.svelte` is the single switch that renders the active one. To add a
modal: extend the union, add a case in `ModalHost`, build the component.
- Mutating modals use `createModalAction()` (`modalAction.svelte.ts`) for the
shared busy/error lifecycle: `action.run(() => runMutation(...), onclose)` —
on success it calls `onclose`, on failure it sets `action.error` and stays
open. Confirm-style modals just wrap `ConfirmModal`.
- Editor modals guard unsaved work with `confirmDiscard(isDirty)` before closing.
- `Modal.svelte` owns a11y: focus trap, focus restore on destroy, Escape/backdrop
close. Don't re-implement dialog behavior per modal.

## Polling

- `createPoller(fn, intervalMs)` (`polling.ts`) self-guards against overlapping
ticks (`inFlight`). `pollersForView(view)` (`pollers.ts`) is a **pure** map of
view → which pollers run, so it's unit-tested independently. `App.svelte` stops
all pollers and starts the view's set on every `navigate`. Poll calls are
wrapped in `safe()` so a transient backend error logs instead of throwing an
unhandled rejection; the next tick retries.

## Charts (`ChartPanel.svelte`)

Chart.js wrapper (`chart.js/auto`). `charts.ts` builds pure, colourless
`ChartSpec`s (`pieSpec`, `tokensBarSpec`); `ChartPanel` is the **only** importer
of `chart.js`/`chartjs-plugin-datalabels`. It resolves the palette
(`--accent`, `--chart-2…6`) and theme colours (`--text-primary`, `--chart-grid`)
from CSS vars and writes them onto the live chart. A **create-effect** builds the
chart once per canvas bind (reads `spec` via `untrack`); a **data-effect** copies
labels/data in place on `spec` change; a **theme-effect** re-applies colours on
`theme.current`. All three end in `chart.update()` — no teardown, so there is no
frappe-style `removeChild` race to suppress. `chartjs-plugin-datalabels` is
registered **per-instance** (`plugins: [ChartDataLabels]`), enabled on the pies
and disabled on the dual-axis bar.

## Testing (Vitest + jsdom + Testing Library)

- Helpers in `tests/helpers.ts`: `stubFetchJson` / `stubFetchRoutes` (substring
URL routing), `errBody` / `okBody` (the two backend channels), `exec()`
fixtures. `tests/setup.ts` restores mocks after each test.
- Test the singleton store via its factory (`createXStore()`) for isolation.
- A store/lib gets a unit test; a component gets a render + interaction test
(assert error-stays-open / success-closes for modals).

## Gotchas

- **Charts use `chart.js/auto`** (auto-registers all controllers/scales — simpler
than manual tree-shaking). `chartjs-plugin-datalabels` is registered per-chart,
not globally. Its Chart.js type augmentation is loaded via the type-only
`src/types/chartjs-datalabels.d.ts`.
- **jsdom has no canvas backend** — component tests that mount a chart must
`vi.mock('chart.js/auto', ...)` and `vi.mock('chartjs-plugin-datalabels', ...)`
(see `tests/chart-panel.test.ts`).
- **Node 26 + Vitest:** Node's experimental `localStorage` shadows jsdom's, so
`tests/setup.ts` reinstalls it; localStorage tests must `clear()` in `beforeEach`.
- `$state(someProp)` (prop as initial state) emits a `state_referenced_locally`
svelte-check **warning**, not an error — suppressed where intentional.
86 changes: 86 additions & 0 deletions dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Serena Dashboard (frontend)

Svelte 5 (runes) + TypeScript + Vite single-page app. It is built into
`../src/serena/resources/dashboard/` and served by the Flask backend in
`../src/serena/dashboard.py` at `/dashboard/`. The backend is a **frozen
contract** — don't change endpoint names, shapes, or ports from here.

> `CLAUDE.md` in this folder has the architecture rules for AI agents; this
> README is the human quick-start.

## Quick start

```bash
cd dashboard
npm install
npm run dev # Vite dev server on :5273, proxies API routes to a backend on :24282
```

`npm run dev` only serves the frontend — start a Serena MCP server (dashboard
enabled, default port **24282**) first so the API calls resolve. Note: the
logo/icon files are served by the backend, so they 404 under `npm run dev`. For
full fidelity (logos, real data, correct routing) build and open the app the way
it ships:

```bash
npm run build # writes hashed assets into ../src/serena/resources/dashboard/
# then open http://localhost:24282/dashboard/ with a Serena server running
```

## Commands

| Command | What |
| --------------------------------- | --------------------------------------------- |
| `npm run check` | Type-check (`svelte-check`) |
| `npm test` / `npm run test:watch` | Vitest |
| `npm run lint` / `npm run format` | ESLint + Prettier (check / write) |
| `npm run build` | Production build into the Python resource dir |

## The build-output contract (important)

The build output under `../src/serena/resources/dashboard/` (`index.html` +
`assets/`) is **committed to git** and shipped in the wheel. After any source
change: **`npm run build` and commit the regenerated output.** CI
(`.github/workflows/dashboard.yml`) rebuilds and fails the PR if the committed
output is stale. There is also a poe task: `poe build-dashboard`.

`npm run build` auto-runs a `prebuild` step (`scripts/clean-assets.mjs`) that
clears `assets/` first. This prevents stale hashed bundles from piling up:
`emptyOutDir: false` is required because the same output dir holds the icon/logo
files, so Vite can't clean it on its own.

**Before committing:** run `npm run format` and **stage all changes**. Committing
only task-scoped files can leave other files prettier-dirty and fail CI's
`prettier --check`.

## Layout

- `src/lib/api/` — `types.ts` (mirrors backend JSON), `endpoints.ts` (one typed
fn per route), `client.ts` (the only place that calls `fetch` for the API).
- `src/lib/stores/` — runes stores (`*.svelte.ts`): config, logs, executions,
stats, theme, modal.
- `src/lib/` — `polling.ts`, `format.ts`, `charts.ts`, `validation.ts`, `banners.ts`.
- `src/components/` — `common/`, `shell/`, `overview/`, `logs/`, `stats/`,
`modals/`, `banners/`.
- `src/styles/tokens.css` — the palette (light + `[data-theme='dark']`). Never
hardcode hex in a component; use `var(--token)`.
- `tests/` — Vitest specs.

## Adding a feature that needs a backend route

1. Add the request/response type to `src/lib/api/types.ts`.
2. Add a typed function to `src/lib/api/endpoints.ts`.
3. Add/extend a store under `src/lib/stores/`.
4. Build the component + a Vitest test, then rebuild and commit the output.

## Gotchas

- **Charts use Chart.js** (`chart.js/auto`) + `chartjs-plugin-datalabels`, wrapped
by `src/components/stats/ChartPanel.svelte` (the only file importing them).
Series colours come from CSS vars, not hardcoded hex.
- **Node 26 + Vitest:** Node's experimental `localStorage` global shadows
jsdom's, so `tests/setup.ts` reinstalls jsdom's Storage. Tests that touch
`localStorage` must `clear()` it in `beforeEach`.
- **`$state(someProp)`** (capturing a prop as initial state) emits a
`state_referenced_locally` svelte-check warning; it's suppressed where
intentional. These are warnings, not errors.
Loading