Files
windmill/frontend/AGENTS.md
T
GuilhemandClaude Opus 5 ac27d0200d feat(sessions): batch edit, filters and grouping in the session sidebar (#10772)
* feat(sessions): batch edit, filters and grouping in the session sidebar

Session list management from the sidebar:

- Edit mode (Settings menu > Edit sessions) puts a checkbox on every row with
  select-all, shift-click range selection, and batch Archive/Unarchive and
  Delete. Batch delete never removes fork workspaces, matching the single
  delete's default.
- Last activity filter (Any time / 7 / 30 / 90 days) hiding sessions untouched
  for longer than the cutoff. Sessions had no activity timestamp, so
  `Session.lastActivityAt` is stamped at the two write funnels (persistTouched,
  markSessionSeen) and falls back to createdAt for older records.
- Group by None / Date (Today, Yesterday, Last 7 days, Last 30 days, Older) /
  Workspace fork, the last giving one group per workspace with a hover "+" that
  starts a session there and a badge for dev workspaces.
- All of it reachable from the collapsed rail's Filter submenu too.

The sidebar header is now a full-width New session button with the settings
menu beside it; the "AI sessions" title stays only in the collapsible sidebar
section, where it doubles as the fold toggle. Entering edit mode reuses that
row and the status-dot slot, so no row moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard batch unarchive and keep new group sessions transient

* style: match session options cog to the new session button size

* style: keep the session options cog neutral regardless of filters

* style: use an ellipsis for the session list options button

* style: narrow the session list options menu to fit the sidebar

* Revert "style: narrow the session list options menu to fit the sidebar"

This reverts commit e602765114.

* fix(sessions): honest archived count and reachable-workspace group actions

* fix(sessions): DST-safe date buckets and a tracked clock for time filters

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:50:37 +02:00

87 lines
4.6 KiB
Markdown

# Frontend (Svelte 5)
- **Coding patterns**: MUST use the `svelte-frontend` skill when writing Svelte code
- **Validation**: `docs/validation.md``npm run check:fast` (2s) for iteration, `npm run check` (50s) for final PR
- **UI components**: use Windmill's design-system components — never raw HTML elements. Start from the barrel `src/lib/components/common/index.ts` and grep `src/lib/components/`; the component you need almost certainly exists
- **Never pass a `@deprecated` prop.** On `<Button>` that means `size`, `spacingSize`, `extendedSize` and the `contained`/`border`/`divider` variants — size buttons with `unifiedSize` (`2xs` | `xs` | `sm` | `md` | `lg`). Deprecated props survive at old call sites; copying one forward is still a bug. Check the prop's JSDoc in the component before using it
- **Brand/design**: `frontend/brand-guidelines.md` — read the relevant section before building UI, not after; the `svelte-frontend` skill maps which section covers what
- **Backend API**: routes in `../backend/windmill-api/openapi.yaml`, generated types in `src/lib/gen/`
- **Regenerate client**: `npm run generate-backend-client` after backend API changes
## Key Frontend Patterns
### Prefer Composable State Over Two-Way Binding
```typescript
// Use resource() from runed for async data
import { resource } from 'runed'
let items = resource(() => args, (args) => SomeService.list(args))
// items.loading, items.current
// Use composables for shared reactive state
function useLoader(argsGetter: () => Args) {
let items = $state([])
let loading = $state(false)
$effect(() => { /* react to argsGetter() */ })
return { get loading() { return loading }, get items() { return items } }
}
```
Two-way binding is fine for simple form inputs. Avoid it for component-to-component state.
## Verifying Frontend Changes
After modifying frontend code, drive the running dev server with the **Playwright MCP** to verify the change in a real browser — don't claim a UI change works without exercising it.
Two MCP servers are registered in `.mcp.json`:
- `playwright` — headless Chromium, default for devboxes (no display required)
- `playwright-headed` — windowed Chromium, when a display is available
**One-time setup:** run `npx playwright install chromium` to download the browser binary (Playwright won't fetch it automatically on first use).
Typical flow:
1. Ensure backend (`cargo run`) and frontend (`REMOTE=http://localhost:8000 npm run dev`) are running
2. `mcp__playwright__browser_navigate` to the relevant page (login at `admin@windmill.dev` / `changeme`)
3. `mcp__playwright__browser_snapshot` to inspect the accessibility tree (preferred over screenshots for reading the DOM)
4. `mcp__playwright__browser_click` / `browser_fill_form` / `browser_type` to interact
5. `mcp__playwright__browser_take_screenshot` for visual confirmation
6. `mcp__playwright__browser_console_messages` / `browser_network_requests` to surface errors
Write screenshots to an absolute path under `/tmp` (the MCP servers already do; standalone
Playwright scripts must be told): moving a PNG out of the checkout afterwards needs a `mv` the
permission hooks always prompt on. Same reason to run `rm`/`mv`/`cp` as one plain command per Bash
call: those hooks defer on `&&`, `;`, redirects, quotes and `$VAR`.
**Attach the screenshots to the PR.** For any change under `frontend/`, embed screenshots of the affected UI in the PR body — the `pr` skill requires this and carries the upload recipe.
If you cannot exercise a UI change (no dev server, etc.), say so explicitly rather than claiming success.
### Traps while driving the UI
- `critical_alerts` 404s are expected on CE builds (EE-only endpoint) — ignore them.
- VSCode worker 404s are dev-mode artifacts — ignore them.
- `<Toggle>` hides its checkbox (`sr-only`). Click the `<label>` wrapper, not the checkbox.
## Banned Patterns
### `$bindable(default_value)` on optional props
Using `$bindable(default_value)` on props that can be `undefined` is **banned**. This pattern causes subtle bugs because the default value masks the `undefined` state.
**Bad:**
```svelte
let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
```
**Correct alternatives:**
1. **Use `$derived` with nullish coalescing** — handle the potential `undefined` at the usage site:
```svelte
let { my_prop = $bindable() }: { my_prop?: string } = $props()
let effective_value = $derived(my_prop ?? default_value)
```
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.