10 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/agents/requirements/profile-disable.md | 2026-08-01 | implemented |
Implementation Plan: Disable an Agent Profile
Overview
Add an enabled flag to agent_profiles (kanban-flavour profiles only),
expose it through the profile DTO/PATCH API, carry it into the frontend
profile type and the store's AgentProfileOption, and filter it out of every
profile selection surface (new task, new subtask, new session, handoff,
quick chat). Two settings toggles: one in the profile page header (dirty/save
flow), one in the /settings/agents profile list (immediate save). Existing
sessions and labels are untouched because filtering happens only where
profiles are offered as choices — the store keeps disabled profiles.
Order: backend persistence/contracts → frontend types + selection filtering → settings UI → E2E. Each layer is independently testable.
Backend
Schema & store (apps/backend/internal/agent/settings/store/sqlite.go)
- Add migration
r.migrate.Apply("agent_profiles.enabled", ALTER TABLE agent_profiles ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1)beside the otheragent_profiles.*migrations inmigrate(). - Add
enabled INTEGER NOT NULL DEFAULT 1to theCREATE TABLE IF NOT EXISTS agent_profilesDDL (both the original table and therecreateAgentProfilesWithoutModelChecknew-table DDL, and include the column in that migration's copy lists so a legacy DB recreation preserves it). CreateAgentProfileINSERT: addenabledcolumn + bind (dialect.BoolToInt(profile.Enabled)).UpdateAgentProfileUPDATE: addenabled = ?to the SET list + bind.agentProfileSelectColumns: addenabled;scanAgentProfile: scan intovar enabled intand setprofile.Enabled = enabled == 1.
Model (apps/backend/internal/agent/settings/models/models.go)
- Add
Enabled bool json:"enabled" db:"enabled"toAgentProfile(nearCLIPassthrough/UserModified).
DTO (apps/backend/internal/agent/settings/dto/dto.go)
- Add
Enabled bool json:"enabled"toAgentProfileDTO.
Controller (apps/backend/internal/agent/settings/controller/profile_crud.go)
CreateProfile: setEnabled: trueon the new profile (Go zero value is false; the DB default only applies when the column is omitted).UpdateProfileRequest: addEnabled *bool.UpdateProfile: applyif req.Enabled != nil { profile.Enabled = *req.Enabled }.toProfileDTO: mapEnabled: profile.Enabled.
Handler (apps/backend/internal/agent/settings/handlers/handlers.go)
updateProfileRequest: addEnabled *bool json:"enabled,omitempty"; passEnabled: body.Enabledinto the controller call.
Frontend
Types & client
apps/web/lib/types/agent-profile.ts: addenabled?: booleantoAgentProfile(identity section) and toAgentProfilePayload.apps/web/lib/api/domains/agent-profile-normalize.ts:normalizeAgentProfilepicksenabledwith fallbacktrue;toAgentProfilePayloadsetsenabled.apps/web/app/actions/agents.ts:updateAgentProfileActionpayload gainsenabled?: boolean.apps/web/lib/state/slices/settings/types.ts:AgentProfileOptiongainsenabled?: boolean;toAgentProfileOptionmapsenabled: profile.enabled ?? true(every call site — settings routes, use-settings-data, WS handlers, SSR, agent pages — picks it up automatically).
Selection filtering (hide disabled profiles from choices only)
-
Define one shared
isSelectableAgentProfile(profile)predicate that returnsprofile.enabled !== false, preserving the enabled-by-default behavior for legacy payloads. Every selector path below must use this helper rather than duplicating the enabled check. -
apps/web/components/task-create-dialog-options.tsx:useAgentProfileOptionsfilters selectable profiles — covers new task, new subtask, new session, and quick-chat option lists. -
apps/web/components/task-create-dialog-computed.ts:useExecutorProfileCompat'scompatibleAgentProfilesmemo filters disabled profiles — feeds autopick (decideAgentProfileAutopicklast-used / workspace-default / first candidates) and thenoCompatibleAgentsubmit gate. -
apps/web/components/task/handoff-profile-menu-items.tsx:useHandoffProfilesfilters disabled profiles. -
apps/web/components/task/new-session-dialog.tsx:useNewSessionDialogStatekeeps the raw current-session profile for existing-session labels and selector visibility, while the new-session initial fallback picks the first selectable profile. -
apps/web/components/quick-chat/quick-chat-setup.tsx: only apply the workspace-defaultdefault_agent_profile_idwhen that profile is enabled, else start unselected.
Settings UI
apps/web/components/settings/agent-profile-page.tsx:ProfileEditorHeadergains anenabledprop and renders a labeledSwitch(@kandev/ui/switch) + helper text ("Disabled profiles stay available for existing sessions but won't be offered when creating new tasks or agents").useProfileEditorStateisDirtyincludesdraft.enabled !== savedProfile.enabled.useProfileSavePATCH payload includesenabled: draft.enabled.- Wire the switch through
ProfileEditor→ header.
apps/web/app/settings/agents/page.tsx:ProfileListItemrenders anenabledSwitchbeside the profile name; clicking it callsupdateAgentProfileAction(profile.id, { enabled: next }), then syncs the store (setSettingsAgentswith the updated profile +setAgentProfilesrebuilt viatoAgentProfileOption) — the same shape asuseSyncAgentsToStorein the profile page. Keep the switch as a sibling outside the row's navigation link so toggling never navigates.
Tests
- What: store persists/reads
enabled(default true, update to false, scan round-trip). File:apps/backend/internal/agent/settings/store/sqlite_profile_enabled_test.go(or extendsqlite_migration_test.go). How: in-memory sqlite repo; create → expectEnabled == true; update →false; re-read →false. - What: migration backfills existing rows to enabled. File: store migration test. How: create DB at pre-migration schema, insert a row without the column, run migrations, read
enabled == 1. - What: PATCH
/api/v1/agent-profiles/:idwith{"enabled": false}persists and returnsenabled: false. File:apps/backend/internal/agent/settings/controller/profile_crud_test.go(+ handler test inhandlers/). How: controller test with fake repo; assert DTO carriesEnabledand repo receives it. - What:
toAgentProfileOptioncarriesenabled. File:apps/web/lib/state/slices/settings/types.test.ts(or existing settings slice test). How: unit test. - What:
normalizeAgentProfiledefaults missingenabledtotrueand mapsfalse. File:apps/web/lib/api/domains/agent-profile-normalize.test.ts. How: unit test. - What:
useAgentProfileOptionsomits disabled profiles. File:apps/web/components/task-create-dialog-options.test.tsx. How: render hook with mixed list, expect only enabled options. - What: autopick never resolves to a disabled profile (last-used / workspace default / first fallback). File:
apps/web/components/task-create-dialog-effects.test.ts(exercisesdecideAgentProfileAutopickvia the compat-filtered list). How: feed a compat list that excludes the disabled profile; assert decision picks an enabled id or "no-compatible". - What: compatibility filtering removes disabled profiles before autopick. File:
apps/web/components/task-create-dialog-computed.test.ts(or the closest dialog-computed test). How: feed mixed raw profiles through the executor compatibility path and assert the returned list contains only enabled profiles. - What:
useHandoffProfilesomits disabled profiles. File:apps/web/components/task/handoff-profile-menu-items.test.ts. How: unit test with mixed store items. - What: profile page header toggle drives dirty state and PATCH payload. File:
apps/web/components/settings/agent-profile-page.test.tsx. How: render with a profile, flip the switch, assertisDirtyand the save action payload includeenabled. - What:
/settings/agentslist toggle calls the update action and refreshes the row. File:apps/web/app/settings/agents/page.test.tsx(or component test forProfileListItem). How: mockupdateAgentProfileAction, click the switch, assert the action payload and store sync.
E2E Tests
- Scenario: GIVEN an existing profile, WHEN the user disables it from the profile settings page and opens the new-task dialog, THEN it is absent from the agent profile selector. File:
apps/web/e2e/tests/settings/agent-profile-disable.spec.ts(pattern fromagent-profile-acp.spec.ts). What to verify: toggle persists (page reload keeps it off), the selector in the task-create dialog does not list the profile, and the profile still appears under "Manage existing profiles by agent" with the toggle off; toggling it back on restores it to the selector.
Implementation Waves And Parallel Candidates
Wave 1:
Wave 2:
Wave 3:
Wave 4:
Risks
- Go zero-value pitfall: a fresh profile must default to
enabled = true— the controller must set it explicitly, otherwise every new profile would be created disabled. - The store's three read paths share
agentProfileSelectColumns; adding the column there plusscanAgentProfilekeeps all reads consistent (the file already documents this as the rule). - The
recreateAgentProfilesWithoutModelCheckmigration rebuilds the table for legacy DBs; forgetting the new column there would silently dropenabledfor upgraded installs. - Disabled + workflow-locked profile: the selector shows "No compatible agent profiles" and blocks submit until the user re-enables the profile or changes the workflow — intended per spec, but worth confirming in review.