Files
kandev/AGENTS.md
T
José Almeida f6f45b5777 feat: add filter/group/sort and saved views to task sidebar (#644)
* feat(web): add sidebar view types and apply-view engine

Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.

* feat(web): add built-in sidebar view presets

Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.

* feat(web): persist sidebar views in ui store

Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.

* feat(web): add filter dimension registry and clause editor

Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.

* feat(web): add sidebar filter bar, popover, and view manager

Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.

* refactor(web): extract NewTaskButton component

Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.

* feat(web): integrate filter/view system into task sidebar

Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.

* test(web): add e2e coverage for sidebar filter/view system

Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.

* refactor(web): trim sidebar built-in views to a single seeded default

Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.

* fix(web): humanize executor type labels in sidebar filter and grouping

The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.

* feat(web): replace "Has PR" filter with "Is PR review"

The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.

The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.

* fix(web): tighten sidebar filter popover layout

Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.

* feat(web): show workflow step color dot in sidebar filter dropdown

Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.

* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown

The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.

* fix(web): show workflow and step names (not UUIDs) in sidebar grouping

Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.

* fix(web): drop redundant true/false selector on boolean sidebar filters

Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.

* refactor(web): drop Sort by Repository and relabel State to Status

Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.

* fix(web): tighten sidebar filter popover section spacing

The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.

* chore(web): apply prettier formatting drift in sidebar-filter area

* revert(web): undo store.ts prettier reformat to stay under max-lines cap

The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).

* refactor(web): label boolean filter ops as Show/Hide instead of is/is not

* fix(web): pull section labels flush to sidebar filter popover borders

* refactor(web): inline rename and save-as into sidebar view header row

* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents

* feat(backend): persist sidebar views in user settings

Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.

* feat(web): hydrate and sync sidebar views to backend

Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.

As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.

* chore(web): apply prettier to store.ts sidebar actions block

* fix(web): run migrateView on SSR-hydrated sidebar views

Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.

* docs: note default-state module and sidebar views in store structure

* fix(web): type sidebar view storage wrappers properly

Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.

* refactor(web): stop PATCHing on sidebar group collapse toggles

collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.

* fix(web): skip sidebar view migration when settings hydrated from server

After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
2026-04-20 13:44:24 +01:00

18 KiB

Kandev Engineering Guide

Purpose: Architecture notes, key patterns, and conventions for LLM agents working on Kandev.

Repo Layout

apps/
├── backend/          # Go backend (orchestrator, lifecycle, agentctl, WS gateway)
├── web/              # Next.js frontend (SSR + WS + Zustand)
├── cli/              # CLI tool (TypeScript)
├── landing/          # Landing page
└── packages/         # Shared packages/types

Tooling

  • Package manager: pnpm workspace (run from apps/, not repo root)
  • Backend: Go with Make (make -C apps/backend test|lint|build)
  • Frontend: Next.js (cd apps && pnpm --filter @kandev/web dev|lint|typecheck)
  • UI: Shadcn components via @kandev/ui
  • GitHub repo: https://github.com/kdlbs/kandev
  • Container image: ghcr.io/kdlbs/kandev (GitHub Container Registry)

Backend Architecture

Package Structure

apps/backend/
├── cmd/
│   ├── kandev/           # Main backend binary entry point
│   ├── agentctl/         # Agentctl binary (runs inside containers or standalone)
│   └── mock-agent/       # Mock agent for testing
├── internal/
│   ├── agent/
│   │   ├── lifecycle/    # Agent instance management (see below)
│   │   ├── agents/       # Agent type implementations
│   │   ├── controller/   # Agent control operations
│   │   ├── credentials/  # Agent credential management
│   │   ├── discovery/    # Agent discovery
│   │   ├── docker/       # Docker-specific agent logic
│   │   ├── dto/          # Agent data transfer objects
│   │   ├── executor/     # Executor types, checks, and service
│   │   ├── handlers/     # Agent event handlers
│   │   ├── registry/     # Agent type registry and defaults
│   │   ├── settings/     # Agent settings
│   │   ├── mcpconfig/    # MCP server configuration
│   │   └── remoteauth/   # Remote auth catalog and method IDs for remote executors/UI
│   ├── agentctl/
│   │   ├── client/       # HTTP client for talking to agentctl
│   │   └── server/       # agentctl HTTP server
│   │       ├── acp/      # ACP protocol implementation
│   │       ├── adapter/  # Protocol adapters + transport/ (ACP, Codex, OpenCode, Copilot, Amp)
│   │       ├── api/      # HTTP endpoints
│   │       ├── config/   # agentctl configuration
│   │       ├── instance/ # Multi-instance management
│   │       ├── mcp/      # MCP server integration
│   │       ├── process/  # Agent subprocess management
│   │       ├── shell/    # Shell session management
│   │       └── utility/  # agentctl utilities
│   ├── orchestrator/     # Task execution coordination
│   │   ├── dto/          # Orchestrator data transfer objects
│   │   ├── executor/     # Launches agents via lifecycle manager
│   │   ├── handlers/     # Orchestrator event handlers
│   │   ├── messagequeue/ # Message queue for agent prompts
│   │   ├── queue/        # Task queue
│   │   ├── scheduler/    # Task scheduling
│   │   └── watcher/      # Event handlers
│   ├── task/
│   │   ├── controller/   # Task HTTP/WS controllers
│   │   ├── dto/          # Task data transfer objects
│   │   ├── events/       # Task event types
│   │   ├── handlers/     # Task event handlers
│   │   ├── models/       # Task, Session, Executor, Message models
│   │   ├── repository/   # Database access (SQLite)
│   │   └── service/      # Task business logic
│   ├── analytics/        # Usage analytics
│   ├── clarification/    # Agent clarification handling
│   ├── common/           # Shared utilities
│   ├── db/               # Database initialization
│   ├── debug/            # Debug tooling
│   ├── editors/          # Editor integration
│   ├── events/           # Event bus for internal pub/sub
│   ├── gateway/          # WebSocket gateway
│   ├── github/           # GitHub API integration (PRs, reviews, webhooks)
│   ├── integration/      # External integrations
│   ├── lsp/              # LSP server
│   ├── mcp/              # MCP protocol support
│   ├── health/           # Health check endpoints
│   ├── notifications/    # Notification system
│   ├── persistence/      # Persistence layer
│   ├── prompts/          # Prompt management
│   ├── repoclone/        # Repository cloning for remote executors
│   ├── scriptengine/     # Script placeholder resolution and interpolation
│   ├── secrets/          # Secret management
│   ├── sprites/          # Sprites AI integration
│   ├── sysprompt/        # System prompt injection
│   ├── task/
│   │   └── ...
│   ├── tools/            # Tool integrations
│   ├── user/             # User management
│   ├── utility/          # Shared utility functions
│   ├── workflow/         # Workflow engine
│   │   ├── engine/       # Typed state-machine engine (trigger evaluation, action callbacks, transition store)
│   │   ├── models/       # Workflow step, template, and history models
│   │   ├── repository/   # Workflow persistence (SQLite)
│   │   └── service/      # Workflow CRUD and step resolution
│   └── worktree/         # Git worktree management for workspace isolation

Key Concepts

Orchestrator coordinates task execution:

  • Receives task start/stop/resume requests via WebSocket
  • Delegates to lifecycle manager for agent operations
  • Handles event-driven state transitions via workflow engine
  • Located in internal/orchestrator/

Workflow Engine (internal/workflow/engine/) provides typed state-machine evaluation:

  • Engine.HandleTrigger() evaluates step actions for triggers (on_enter, on_turn_start, on_turn_complete, on_exit)
  • TransitionStore interface abstracts persistence (implemented by orchestrator.workflowStore)
  • CallbackRegistry maps action kinds to callbacks (plan mode, auto-start, context reset)
  • First-transition-wins: multiple transition actions in one trigger, first eligible wins
  • EvaluateOnly mode: engine evaluates without persisting, caller orchestrates on_exit → DB → on_enter
  • RequiresApproval on actions: transitions requiring review gating are skipped
  • Idempotent by OperationID; session-scoped data bag via MachineState.Data

Lifecycle Manager (internal/agent/lifecycle/) manages agent instances:

  • Manager (manager.go, manager_*.go) - central coordinator for agent lifecycle
  • ExecutorBackend interface (executor_backend.go) - abstracts execution environment (Docker, Standalone, Sprites, Remote Docker)
  • ExecutionStore (execution_store.go) - thread-safe in-memory execution tracking
  • session.go - ACP session initialization and resume
  • streams.go - WebSocket stream connections to agentctl
  • process_runner.go - agent process launch and management
  • profile_resolver.go - resolves agent profiles/settings

agentctl is an HTTP server that:

  • Runs inside Docker containers or as standalone process
  • Manages agent subprocess via stdin/stdout (ACP protocol)
  • Exposes workspace operations (shell, git, files)
  • Supports multiple concurrent instances on different ports

Executor Types (database model):

  • local_pc - Standalone process on host ✅
  • local_docker - Docker container on host ✅
  • sprites - Sprites cloud environment ✅
  • remote_docker, remote_vps, k8s - Planned

Execution Flow

Client (WS) → Orchestrator → Lifecycle Manager → ExecutorBackend (container/process) → agentctl
                                                                                          ↓
Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream updates (WS) ──────── agent subprocess
  1. Orchestrator receives task.start via WS
  2. Lifecycle Manager creates executor instance (container or process)
  3. agentctl starts inside the instance, agent subprocess is configured and started
  4. Agent events stream back via WS through the chain

Session Resume: TaskSession.ACPSessionID stored for resume; ExecutorRunning tracks active state; on restart RecoverInstances() reconnects.

Provider Pattern: Packages expose Provide(cfg, log) (*impl, cleanup, error) for DI. Returns implementation, cleanup function, and error. Cleanup called during graceful shutdown.

Worktrees: internal/worktree/Manager provides workspace isolation. Each session can have its own worktree (branch) to prevent conflicts between concurrent agents.

Executor default scripts: Default prepare scripts are in internal/agent/lifecycle/default_scripts.go; internal/scriptengine/ handles placeholder resolution.


agentctl Server

API Groups

agentctl exposes these route groups (see internal/agentctl/server/api/):

  • /health, /info, /status - Health and status
  • /instances/* - Multi-instance management
  • /processes/* - Agent subprocess management (start/stop)
  • /agent/configure, /agent/stream - Agent configuration and event streaming
  • /git/* - Git operations (status, commit, push, pull, rebase, stage, etc.)
  • /shell/* - Shell session management
  • /workspace/* - File operations, search, tree
  • /vscode/* - VS Code integration proxy

Adapter Model

Protocol adapters in adapter/transport/ normalize different agent CLIs:

  • AgentAdapter interface defines Start(), Stop(), Prompt(), Cancel()
  • Transports: acp (Claude Code), codex (OpenAI Codex), opencode, shared, streamjson
  • Top-level adapters: CopilotAdapter (GitHub Copilot SDK), AmpAdapter (Sourcegraph Amp)
  • process.Manager owns subprocess, wires stdio to adapter
  • Factory pattern in adapter/factory.go selects adapter by agent type

ACP Protocol

JSON-RPC 2.0 over stdin/stdout between agentctl and agent process. Requests: initialize, session/new, session/load, session/prompt, session/cancel. Notifications: session/update with types message_chunk, tool_call, tool_update, complete, error, permission_request, context_window.


Frontend Architecture

UI Components

Shadcn Components: Import from @kandev/ui package:

import { Badge } from '@kandev/ui/badge';
import { Button } from '@kandev/ui/button';
import { Dialog } from '@kandev/ui/dialog';
// etc...

Do NOT import from @/components/ui/* - always use @kandev/ui package.

Data Flow Pattern (Critical)

SSR Fetch -> Hydrate Store -> Components Read Store -> Hooks Subscribe

Never fetch data directly in components.

Store Structure (Domain Slices)

lib/state/
├── store.ts                        # Root composition
├── default-state.ts                # Default state + initial state merge
├── slices/                         # Domain slices
│   ├── kanban/                    # boards, tasks, columns
│   ├── session/                   # sessions, messages, turns, worktrees
│   ├── session-runtime/           # shell, processes, git, context
│   ├── workspace/                 # workspaces, repos, branches
│   ├── settings/                  # executors, agents, editors, prompts (incl. userSettings)
│   ├── comments/                  # code review diff comments
│   ├── github/                    # GitHub PRs, reviews
│   └── ui/                        # preview, connection, active state, sidebar views
├── hydration/                     # SSR merge strategies

hooks/domains/{kanban,session,workspace,settings,comments,github}/  # Domain-organized hooks
lib/api/domains/                    # API clients
├── kanban-api, session-api, workspace-api, settings-api, process-api
├── plan-api, queue-api, workflow-api, stats-api, github-api
├── user-shell-api, debug-api, secrets-api, sprites-api, vscode-api
├── health-api, utility-api

Key State Paths:

  • messages.bySession[sessionId], shell.outputs[sessionId], gitStatus.bySessionId[sessionId]
  • tasks.activeTaskId, tasks.activeSessionId, workspaces.activeId
  • repositories.byWorkspace, repositoryBranches.byRepository

Hydration: lib/state/hydration/merge-strategies.ts has deepMerge(), mergeSessionMap(), mergeLoadingState() to avoid overwriting live client state. Pass activeSessionId to protect active sessions.

Hooks Pattern: Hooks in hooks/domains/ encapsulate WS subscription + store selection. WS client deduplicates subscriptions automatically.

WS

Format: {id, type, action, payload, timestamp}.


Best Practices

Commit Conventions (enforced by CI)

Commits to main must follow Conventional Commits (type: description). PRs are squash-merged — the PR title becomes the commit, validated by CI. Changelog is auto-generated from these via git-cliff (cliff.toml). See .agents/skills/commit/SKILL.md for allowed types and examples.

Code Quality (enforced by linters)

Static analysis runs in CI and pre-commit. New code must stay within these limits:

Go (apps/backend/.golangci.yml - errors on new code only):

  • Functions: ≤80 lines, ≤50 statements
  • Cyclomatic complexity: ≤15 · Cognitive complexity: ≤30
  • Nesting depth: ≤5 · Naked returns only in functions ≤30 lines
  • No duplicated blocks (≥150 tokens) · Repeated strings → constants (≥3 occurrences)

TypeScript (apps/web/eslint.config.mjs - warnings, will become errors):

  • Files: ≤600 lines · Functions: ≤100 lines
  • Cyclomatic complexity: ≤15 · Cognitive complexity: ≤20
  • Nesting depth: ≤4 · Parameters: ≤5
  • No duplicated strings (≥4 occurrences) · No identical functions · No unused imports
  • No nested ternaries

When you hit a limit: extract a helper function, custom hook, or sub-component. Prefer composition over growing a single function.

Testing

Every code change must include tests for new or changed logic. Backend: *_test.go files alongside the source. Frontend: *.test.ts files for utility functions, hooks, API clients, and store slices. Exceptions: config files, generated code, React component markup. Use /tdd for test-driven development.

Knowledge

  • Decisions: Architecture decisions are recorded in docs/decisions/. Read docs/decisions/INDEX.md for an overview. When making significant architectural choices, create a new ADR via /record decision.
  • Plans: Implementation plans are stored in docs/plans/. Save completed feature designs via /record plan.

Backend

  • Provider pattern for DI; stderr for logs, stdout for ACP only
  • Pass context through chains; event bus for cross-component comm
  • Execution access: Workspace-oriented handlers (files, shell, inference, ports, vscode, LSP) MUST use GetOrEnsureExecution(ctx, sessionID) — it recovers from backend restarts by creating executions on-demand. Only use GetExecutionBySessionID for operations that require a running agent process (prompt, cancel, mode).
  • Testing: Prefer testing/synctest (Go 1.24+) over time.Sleep for time-dependent tests. Use synctest.Test to wrap tests with tickers or timeouts — it advances fake time instantly when all goroutines are idle. When synctest is not feasible (e.g., tests spawning external processes like git), use channel-based synchronization (<-started, non-blocking select) instead of sleep-based waits. Reserve time.Sleep only for integration tests that need real subprocess execution time.

Frontend

  • Data: SSR fetch → hydrate → read store. Never fetch in components
  • UI Components:
    • Import shadcn components from @kandev/ui, NOT @/components/ui/*
    • Always prefer native shadcn components over custom implementations
    • Check apps/packages/ui/src/ for available components (pagination, table, dialog, etc.)
    • For data tables, use @kandev/ui/table with TanStack Table; use shadcn Pagination components
    • Only create custom components when shadcn doesn't provide what's needed
  • Components: <200 lines, extract to domain components, composition over props
  • Hooks: Domain-organized in hooks/domains/, encapsulate subscription + selection
  • WS: Use subscription hooks only; client auto-deduplicates
  • Interactivity: All buttons and links with actions must have cursor-pointer class

Plan Implementation

  • After implementing a plan, run make fmt first to format code, then run make typecheck test lint to verify the changes. Formatting must come first because formatters may split lines, which can trigger complexity linter warnings.

GitHub Operations

Skills use gh CLI by default. If a gh command fails (not installed, not authenticated, etc.), use whatever GitHub tools are available in the environment (MCP GitHub tools, API tools, etc.) to accomplish the same operation. The goal is the same — the tool may differ.


Maintaining This File

This file is read by AI coding agents (Claude Code via CLAUDE.md symlink, Codex via AGENTS.md). If your changes make any section of this file outdated or inaccurate - e.g., you add/remove/rename packages, change architectural patterns, add new adapters, modify store slices, or change conventions - update the relevant sections of this file as part of the same PR. Keep descriptions concise and factual. Do not add speculative or aspirational content.


Last Updated: 2026-03-05