Skip to content

Commit 8f51bf9

Browse files
paul-basanetsclaude
andcommitted
fix(dashboard): functional, a11y & type-check bug sweep
Final correctness and quality pass. - Route diagnostics per-file to the correct language server. - Functional + a11y bug sweep across all tabs. - Resolve poe type-check (mypy) errors. - Enforce Svelte 5 patterns in CLAUDE.md and add a Serena memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be957b3 commit 8f51bf9

55 files changed

Lines changed: 695 additions & 22406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.serena/memories/core.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Serena is an MCP-based "IDE for coding agents": semantic code retrieval/editing/
1111
- `config/``serena_config.py`, `context_mode.py`, `client_setup.py`
1212
- `resources/config/contexts/*.yml`, `resources/config/modes/*.yml` — context/mode definitions
1313
- `code_editor.py`, `symbol.py`, `ls_manager.py` — symbolic editing / LS lifecycle
14-
- `dashboard.py`, `gui_log_viewer.py` — web dashboard / log viewer
14+
- `dashboard.py`, `gui_log_viewer.py` — web dashboard backend (frozen API) / log viewer; the Svelte frontend lives in `dashboard/` — see `mem:dashboard_frontend`
1515
- `prompt_factory.py` + `generated/generated_prompt_factory.py` — prompts (regenerate with `scripts/gen_prompt_factory.py`)
1616
- `src/solidlsp/` — LSP client framework; per-language servers under `language_servers/`
1717
- `src/interprompt/` — prompt template library (synced from external repo; see `.syncCommitId.*`)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Dashboard Frontend
2+
3+
Svelte 5 (runes) + TS + Vite SPA in `dashboard/`. Built into
4+
`src/serena/resources/dashboard/` and served by Flask in `src/serena/dashboard.py`
5+
at `/dashboard/`. **`dashboard/CLAUDE.md` is the authoritative ruleset** — read it
6+
before editing anything under `dashboard/`; this memory only records the
7+
hard invariants.
8+
9+
## Hard invariants
10+
11+
- **Backend is a frozen contract.** `dashboard.py` endpoint names, request/response
12+
shapes, ports, and the host-header check must not change from the frontend.
13+
Canonical route list = `API_ROUTES` in `dashboard/vite.config.ts`.
14+
- **Build output is committed & CI-enforced.** After any `dashboard/src` change:
15+
`npm run build` (writes hashed assets to `src/serena/resources/dashboard/`) and
16+
commit the regenerated `index.html` + `assets/`. Stale output fails CI
17+
(`.github/workflows/dashboard.yml`; poe task `poe build-dashboard`).
18+
- Before committing dashboard work: `npm run format` and **stage all changes** — a
19+
partial stage leaves files prettier-dirty and CI's `prettier --check` fails.
20+
- `prebuild` (`scripts/clean-assets.mjs`) clears `assets/` because
21+
`emptyOutDir: false` (the dir also holds icon/logo PNGs).
22+
23+
## Commands (run from `dashboard/`)
24+
25+
`npm run dev` (Vite :5273, proxies API to a Serena server on :24282) ·
26+
`npm run build` · `npm run check` (svelte-check) · `npm test` (Vitest) ·
27+
`npm run lint` / `npm run format`. Dev needs a running Serena MCP server with the
28+
dashboard enabled; logos/icons 404 under dev (backend-served).
29+
30+
## Architecture invariants
31+
32+
- Layered: `lib/api/` (`types.ts``endpoints.ts``client.ts`, the only `fetch`)
33+
`lib/stores/*.svelte.ts` (runes singletons, getter-only) → components. Never
34+
`fetch` from a component.
35+
- Two backend failure channels (non-2xx `ApiError` AND HTTP-200
36+
`{status:'error'}`); normalize every mutation through `runMutation()`.
37+
- `$derived` for computed values; `$effect` only for true side effects (never to
38+
sync state). Reactive collections use `SvelteSet`/`SvelteMap`. Colors via
39+
`var(--token)` from `styles/tokens.css`, never hardcoded. Charts only through
40+
`ChartPanel.svelte`. Snippets, not `<slot>`. No jQuery.
41+
- Each store/lib gets a unit test; each component a render+interaction test.
42+
Vitest + jsdom; chart-mounting tests must mock `chart.js/auto`.

.serena/project.yml

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,24 @@ project_name: "serena"
33

44

55
# list of languages for which language servers are started; choose from:
6-
# al angular ansible bash clojure
7-
# cpp cpp_ccls crystal csharp csharp_omnisharp
8-
# dart elixir elm erlang fortran
9-
# fsharp go groovy haskell haxe
10-
# hlsl html java json julia
11-
# kotlin lean4 lua luau markdown
12-
# matlab msl nix ocaml pascal
13-
# perl php php_phpactor powershell python
14-
# python_jedi python_ty r rego ruby
15-
# ruby_solargraph rust scala scss solidity
16-
# svelte swift systemverilog terraform toml
17-
# typescript typescript_vts vue yaml zig
6+
# al ansible bash clojure cpp
7+
# cpp_ccls crystal csharp csharp_omnisharp dart
8+
# elixir elm erlang fortran fsharp
9+
# go groovy haskell haxe hlsl
10+
# java json julia kotlin lean4
11+
# lua luau markdown matlab msl
12+
# nix ocaml pascal perl php
13+
# php_phpactor powershell python python_jedi python_ty
14+
# r rego ruby ruby_solargraph rust
15+
# scala solidity swift systemverilog terraform
16+
# toml typescript typescript_vts vue yaml
17+
# zig
1818
# (This list may be outdated. For the current list, see values of Language enum here:
1919
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
2020
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
2121
# Note:
2222
# - For C, use cpp
2323
# - For JavaScript, use typescript
24-
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
25-
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
26-
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
2724
# - For Free Pascal/Lazarus, use pascal
2825
# Special requirements:
2926
# Some languages require additional setup/installations.
@@ -33,7 +30,7 @@ project_name: "serena"
3330
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
3431
languages:
3532
- python
36-
- svelte
33+
- typescript
3734

3835
# whether to use project's .gitignore files to ignore files
3936
ignore_all_files_in_gitignore: true

dashboard/CLAUDE.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,41 @@ and fail CI's `prettier --check`). `prebuild` (`scripts/clean-assets.mjs`) clear
2626

2727
## Architecture rules
2828

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

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

4166
`$state` only lives in `.svelte.ts` modules. Each store is a factory returning
@@ -59,6 +84,14 @@ export const x = createXStore(); // import the singleton; factory exists for tes
5984
Never expose the `$state` variable directly — only getters, so reads stay
6085
reactive and writes funnel through methods.
6186

87+
- Reactive `Set`/`Map` use `SvelteSet`/`SvelteMap` from `svelte/reactivity` (see
88+
`expanded` in `code.svelte.ts`) — plain `Set`/`Map` mutations don't trigger
89+
updates.
90+
- Guard overlapping async writes with a plain (non-reactive) **epoch counter**:
91+
bump + snapshot it before the `await`, then drop the result if the snapshot is
92+
stale (see `searchEpoch`/`diagEpoch` in `code.svelte.ts`). Stops a slow earlier
93+
request from clobbering a newer one.
94+
6295
## API layer (`src/lib/api/`)
6396

6497
- `types.ts` — TS mirrors of backend JSON. `endpoints.ts` — one typed fn per

dashboard/src/components/code/DiagnosticsPanel.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@
182182
</div>
183183
{/if}
184184

185+
{#if code.diag_skipped_unsupported > 0 && !code.diag_loading}
186+
<div class="skipped" role="note">
187+
{code.diag_skipped_unsupported}
188+
{code.diag_skipped_unsupported === 1 ? 'file was' : 'files were'} not analyzed — no language server
189+
is running for {code.diag_skipped_unsupported === 1 ? 'its type' : 'their types'}.
190+
</div>
191+
{/if}
192+
185193
{#if code.diag_files.length === 0 && !code.diag_loading && !code.diag_error}
186194
{#if code.diag_last_scope}
187195
<p class="empty">
@@ -331,6 +339,15 @@
331339
padding: var(--space-2);
332340
margin-bottom: var(--space-2);
333341
}
342+
.skipped {
343+
background: var(--bg);
344+
border: 1px solid var(--border);
345+
border-radius: var(--radius);
346+
padding: var(--space-1) var(--space-2);
347+
margin-bottom: var(--space-2);
348+
color: var(--text-secondary);
349+
font-size: 0.85em;
350+
}
334351
.error-card {
335352
border-color: var(--log-error);
336353
color: var(--log-error);

dashboard/src/components/code/WorkspaceSearch.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
{/if}
3838
{#if code.search_results.length > 0}
3939
<ul class="results">
40-
{#each code.search_results as m (m.path + ':' + m.name + ':' + m.range.start.line)}
40+
{#each code.search_results as m (m.path + ':' + m.name + ':' + m.range.start.line + ':' + m.range.start.character)}
4141
<li>
4242
<button type="button" onclick={() => selectMatch(m.path)} class="match">
4343
<span class="kind">{m.kind}</span>

dashboard/src/components/common/Modal.svelte

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
<script module lang="ts">
2+
// Per-instance counter for unique title ids (so aria-labelledby resolves even
3+
// with multiple modals mounted across a session).
4+
let modalUid = 0;
5+
</script>
6+
17
<script lang="ts">
28
import type { Snippet } from 'svelte';
39
import { onDestroy } from 'svelte';
@@ -17,6 +23,7 @@
1723
1824
let contentEl = $state<HTMLDivElement | null>(null);
1925
let previouslyFocused: HTMLElement | null = null;
26+
const titleId = `modal-title-${modalUid++}`;
2027
2128
// Move focus into the dialog when it opens, and restore it to the trigger on close,
2229
// so keyboard/screen-reader users land inside the modal instead of behind it.
@@ -41,10 +48,14 @@
4148
const first = focusable[0];
4249
const last = focusable[focusable.length - 1];
4350
const activeEl = document.activeElement;
44-
if (e.shiftKey && activeEl === first) {
51+
// Initial focus sits on the dialog container (tabindex=-1), which is NOT in
52+
// the focusable list. Treat "focus not within the list" as a boundary too,
53+
// otherwise the very first Shift+Tab escapes the trap to the page behind.
54+
const withinList = Array.prototype.indexOf.call(focusable, activeEl) !== -1;
55+
if (e.shiftKey && (activeEl === first || !withinList)) {
4556
e.preventDefault();
4657
last.focus();
47-
} else if (!e.shiftKey && activeEl === last) {
58+
} else if (!e.shiftKey && (activeEl === last || !withinList)) {
4859
e.preventDefault();
4960
first.focus();
5061
}
@@ -59,6 +70,7 @@
5970
class="modal-content"
6071
role="dialog"
6172
aria-modal="true"
73+
aria-labelledby={title ? titleId : undefined}
6274
tabindex="-1"
6375
bind:this={contentEl}
6476
onclick={(e) => e.stopPropagation()}
@@ -71,7 +83,7 @@
7183
>
7284
<button type="button" class="modal-close" aria-label="Close" onclick={onclose}>&times;</button
7385
>
74-
{#if title}<h3>{title}</h3>{/if}
86+
{#if title}<h3 id={titleId}>{title}</h3>{/if}
7587
{#if error}<p class="modal-error" role="alert">{error}</p>{/if}
7688
{@render children()}
7789
</div>

dashboard/src/components/stats/ChartPanel.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
cssVar('--chart-3', '#7fb77e'),
3232
cssVar('--chart-4', '#d88c8c'),
3333
cssVar('--chart-5', '#b39ddb'),
34-
cssVar('--chart-6', '#e0a458'),
34+
cssVar('--chart-6', '#4db6ac'),
35+
cssVar('--chart-7', '#e57aa8'),
36+
cssVar('--chart-8', '#a1887f'),
3537
];
3638
}
3739

dashboard/src/components/stats/RateChart.svelte

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
type WindowMinutes = 15 | 30 | 60 | 360;
77
let windowMinutes = $state<WindowMinutes>(15);
88
let stacked = $state(false);
9-
let enabledTools = $state<Set<string>>(new Set());
9+
// null = "all tools" (the default, and what "Show all" restores — new tools
10+
// auto-included). An explicit Set is the user's selection; an empty Set means
11+
// "Disable all" (show none), which a plain empty Set could not distinguish
12+
// from the initial state.
13+
let enabledTools = $state<Set<string> | null>(null);
1014
1115
const allTools = $derived(Array.from(new Set(timeline.records.map((r) => r.tool))).sort());
12-
// When stacked is on and the user hasn't picked any tool, show all of them.
13-
const activeTools = $derived(
14-
stacked ? allTools.filter((t) => enabledTools.size === 0 || enabledTools.has(t)) : [],
15-
);
16+
const activeTools = $derived.by(() => {
17+
if (!stacked) return [];
18+
const sel = enabledTools;
19+
return sel === null ? allTools : allTools.filter((t) => sel.has(t));
20+
});
1621
1722
// Re-bucket on every tick so the current minute keeps catching live calls
1823
// (B17 — current minute is the LAST bucket, never dropped).
@@ -27,7 +32,7 @@
2732
const spec = $derived(rateChartSpec(timeline.records, nowS, windowMinutes, stacked, activeTools));
2833
2934
function showAll() {
30-
enabledTools = new Set(allTools);
35+
enabledTools = null;
3136
}
3237
function disableAll() {
3338
enabledTools = new Set();

dashboard/src/lib/api/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,8 @@ export interface FileDiagnostics {
211211
export interface ResponseDiagnosticsSummary {
212212
files: FileDiagnostics[];
213213
truncated: boolean;
214+
// Count of in-scope files no language server handles (e.g. Markdown/JSON in a
215+
// Python-only project). Skipped rather than mis-linted; optional for backward
216+
// compatibility with older backends that omit it.
217+
skipped_unsupported?: number;
214218
}

0 commit comments

Comments
 (0)