Files
windmill/frontend/AGENTS.md
T
Guilhem 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

4.6 KiB

Frontend (Svelte 5)

  • Coding patterns: MUST use the svelte-frontend skill when writing Svelte code
  • Validation: docs/validation.mdnpm 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

// 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:

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:

    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.