* 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>
4.6 KiB
Frontend (Svelte 5)
- Coding patterns: MUST use the
svelte-frontendskill 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.tsand grepsrc/lib/components/; the component you need almost certainly exists - Never pass a
@deprecatedprop. On<Button>that meanssize,spacingSize,extendedSizeand thecontained/border/dividervariants — size buttons withunifiedSize(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; thesvelte-frontendskill maps which section covers what - Backend API: routes in
../backend/windmill-api/openapi.yaml, generated types insrc/lib/gen/ - Regenerate client:
npm run generate-backend-clientafter 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:
- Ensure backend (
cargo run) and frontend (REMOTE=http://localhost:8000 npm run dev) are running mcp__playwright__browser_navigateto the relevant page (login atadmin@windmill.dev/changeme)mcp__playwright__browser_snapshotto inspect the accessibility tree (preferred over screenshots for reading the DOM)mcp__playwright__browser_click/browser_fill_form/browser_typeto interactmcp__playwright__browser_take_screenshotfor visual confirmationmcp__playwright__browser_console_messages/browser_network_requeststo 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_alerts404s 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:
-
Use
$derivedwith nullish coalescing — handle the potentialundefinedat the usage site:let { my_prop = $bindable() }: { my_prop?: string } = $props() let effective_value = $derived(my_prop ?? default_value) -
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.