Files
kandev/docs/plans/task-create-agent-compatibility-recovery/plan.md
T
Jonathan Marbutt 97140168d8 fix(web): keep a usable agent selected when the executor changes (#3388)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-05 17:42:06 +00:00

13 KiB

created, status, requirements, system_design, legacy_specs
created status requirements system_design legacy_specs
2026-09-04 implemented
REQ-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001
../../specs/tasks/system-design/task-create-agent-executor-compatibility.md

Implementation Plan: Task Create Agent Compatibility Recovery

Overview

Fix issue #3387: the task-create dialog shows "No compatible agent profiles for X" and hides the agent selector whenever the selected agent is incompatible with the executor, even when other agents are compatible. The result is a dialog that replaces an incompatible selection automatically, keeps the selector visible whenever a compatible profile exists, and names the workflow, agent, and executor when a workflow lock prevents recovery.

Order: derive the state and the automatic replacement first (pure logic with unit tests), then the presentation and copy that depend on that state, then the end-to-end evidence. Each step is independently verifiable.

Confirmed root cause

  • useExecutorProfileCompat in apps/web/components/task-create-dialog-computed.ts folds two conditions into one boolean, noCompatibleAgent: the compatible list is empty, or the selected agent is absent from it.
  • AgentColumn in apps/web/components/task-create-dialog-form-body.tsx renders the same "No compatible agent profiles" empty state for both conditions and drops the selector, so the second condition has no recovery path in the UI.
  • handleExecutorProfileChange in apps/web/components/task-create-dialog-handlers.ts only stores the executor id, and decideAgentProfileAutopick in apps/web/components/task-create-dialog-autopick.ts skips with already-set whenever an agent id is present, so nothing re-validates the agent after an executor switch.
  • The compatibility rule in apps/web/lib/agent-executor-compat.ts is correct. The user's Sprites profile had Claude configured, but the pre-selected agent was a different agent type.

Reproduction without a browser: the existing useDefaultSelectionsEffect tests in apps/web/components/task-create-dialog-effects.test.ts only cover an empty agentProfileId. A new case with agentProfileId set to an incompatible profile and a non-empty compatible list fails before the fix because setAgentProfileId is never called.

Scope

In scope

  • A compatibility state derived in the dialog's computed hook, including a truthful pending state for an unavailable selection.
  • Automatic replacement of an incompatible, non-locked selection using the existing preference order.
  • Selector kept visible whenever a compatible profile exists.
  • A workflow-locked message naming workflow, agent, and executor.
  • Canonical submit preflight for the form, Ctrl/Cmd+Enter shortcut, and programmatic composer path.
  • Footer disabled reasons that match the shown state.
  • Translated copy in all five locales plus the regenerated pseudo locale, including sentence-start and inline fallback forms.
  • Desktop and mobile Playwright evidence for the user-visible states.

Out of scope

  • Marking incompatible executors in the executor selector.
  • Changing the credential compatibility rule.
  • The additional-session, handoff, and quick-chat dialogs.
  • Persisting the automatic replacement as the last-used agent.

Technical approach

Compatibility state and replacement (task-create-dialog-computed.ts, task-create-dialog-autopick.ts, task-create-dialog-types.ts)

  • Add AgentCompatState and the pure computeAgentCompatState helper next to filterCompatibleAgentProfiles. useExecutorProfileCompat returns agentCompatState, keeps noCompatibleAgent as agentCompatState !== "compatible", and returns the effective agent profile's label as selectedAgentProfileName.
  • Extend AutopickDecision's pick variant with replaces?: string. In getAgentAutopickGate, evaluate the replacement gate before the already-set skip. decideAgentProfileAutopick reuses the last-used, workspace-default, first-compatible candidate chain for the replacement.
  • buildAgentAutopickDebugFields logs replaces.
  • DialogComputedValues gains agentCompatState and selectedAgentProfileName.

Presentation and copy (task-create-dialog-form-body.tsx, task-create-dialog-prop-builders.ts, task-create-dialog-footer.tsx, locales)

  • AgentColumn branches on agentCompatState and workflowAgentLocked per the presentation table in the system design. The incompatible note carries data-testid="agent-profile-incompatible-note"; an unavailable selection uses a separate truthful note and keeps the selector visible.
  • buildFormBodyProps forwards agentCompatState, selectedAgentProfileName, and effectiveWorkflowName (looked up from setup.workflows by computed.effectiveWorkflowId).
  • computeDisabledReason returns state-specific keys for selected-incompatible and selected-unavailable; resolveDisabledReason accepts the agent name.
  • Add agentNotConfiguredOnExecutor, workflowAgentNotConfiguredOnExecutor, selectedAgentNotConfiguredFor, selectedAgentProfileFallback, selectedAgentProfileFallbackInline, and selectedAgentProfileUnavailable to every task.json catalog.

Submit preflight (task-create-dialog-setup.ts, setup tests)

  • Pass the derived compatibility block into the canonical guarded submit handler, so the form, Ctrl/Cmd+Enter shortcut, and programmatic submit cannot bypass the footer's disabled state.

End-to-end evidence (apps/web/e2e/tests/task/)

  • The E2E backend registers the mock agent as its only agent type, so every profile shares one compatibility result until the backend is restarted with KANDEV_MOCK_PROVIDERS: "codex-acp", which registers a mocked second agent type (the pattern executor-agent-config.spec.ts already uses). A shared helper, agent-compatibility-helpers.ts, performs that setup: a Codex-alias profile, a Docker executor profile without credentials, and a /api/v1/remote-credentials route mock that requires an env secret for the seeded agent and nothing for the alias.
  • Desktop: extend create-task.spec.ts with two cases: the seeded agent is picked on the default executor and replaced by the Codex profile after switching to Docker; and a workflow that pins the seeded profile shows the workflow-locked note on Docker.
  • Mobile: cover both the workflow-locked flow and the unlocked automatic replacement at a phone viewport. Assert the note, the tappable credentials link, the disabled or enabled start action, and no horizontal document overflow.

Tests

Acceptance criterion Evidence
001.1, 001.6 task-create-dialog-computed.test.ts: computeAgentCompatState returns selected-incompatible or selected-unavailable, not a false none-compatible, when a compatible profile exists. task-create-dialog-form-body.test.tsx: CreateEditSelectors renders the selector and a truthful note, never the empty state, in those states.
001.2, 001.8 task-create-dialog-effects.test.ts: decideAgentProfileAutopick returns a pick with replaces for an incompatible non-locked selection and honors last-used, default, and first order; useDefaultSelectionsEffect calls setAgentProfileId with the replacement and never calls syncTaskCreateLastUsed.
001.3 task-create-dialog-effects.test.ts: decideAgentProfileAutopick skips with already-set when the selection is compatible.
001.4 Existing CreateEditSelectors empty-state test in task-create-dialog-form-body.test.tsx and the existing Docker E2E case.
001.5 task-create-dialog-form-body.test.tsx: workflow-locked note names workflow, agent, and executor, with the credentials link. task-create-dialog-effects.test.ts: no replacement when the workflow locks the agent.
001.7 task-create-dialog-footer.test.ts: computeDisabledReason and resolveDisabledReason for both states.
Submit guard task-create-dialog-setup.test.ts: form and keyboard submission are prevented for selected-incompatible and none-compatible.
001.9 Mobile Playwright spec below.

E2E tests

Flow AC File and project
Docker executor without credentials shows the empty state and disables start (existing). 001.4 tests/task/create-task.spec.ts, chromium
Seeded agent picked on the default executor is replaced by the compatible Codex profile after switching to Docker; no empty state or note; start enabled. 001.1, 001.2, 001.6 tests/task/create-task.spec.ts, chromium
Workflow-locked agent on a Docker executor without credentials shows the workflow note, keeps the link, disables start. 001.5, 001.6, 001.7 tests/task/create-task.spec.ts, chromium
Same workflow-locked flow on a phone viewport, note wraps, link is tapped, no horizontal overflow. 001.9 tests/task/mobile-create-task-agent-compatibility.spec.ts, mobile-chrome
Unlocked incompatible selection is replaced on a phone and start becomes enabled. 001.1, 001.2, 001.6, 001.9 tests/task/mobile-create-task-agent-compatibility.spec.ts, mobile-chrome

Work orders

Dependency order

Task 01 -> Task 02 -> Task 03

Task 02 consumes the state contract from Task 01. Task 03 exercises the rendered result of Task 02 against the production build.

Verification results

  • Task 01: pnpm exec vitest run on the computed, effects, workflow-agent effect, and effects-executor files: 4 files, 66 tests passed. Typecheck and eslint clean.
  • Task 02: pnpm exec vitest run on form-body, footer, prop-builders plus the Task 01 files, dialog, and setup tests: 7 files, 116 tests passed. pnpm run i18n:check, pnpm run typecheck, and eslint on every changed production and test file: clean.
  • Task 03: pnpm e2e:raw tests/task/create-task.spec.ts: 17 passed. pnpm e2e:raw --project=mobile-chrome tests/task/mobile-create-task-agent-compatibility.spec.ts: 1 passed.
  • Package gates: pnpm run i18n:ratchet, node scripts/check-new-e2e-sleeps.mjs, check-no-em-dash-ui.mjs, prettier, python3.13 scripts/lint-spec-files.py --all, and python3.13 scripts/lint-architecture.py --all: clean.

Risks

  • The replacement effect must not fight the workflow lock effect. The gate checks both workflowAgentProfileId and the synchronous workflowHasAgent lookup, matching the existing autopick guard.
  • A profile-list refresh could momentarily drop the selected profile. The gate requires a non-empty compatible list, so an empty refresh cannot clear a selection; a non-empty list that lacks the selection is a real incompatibility or a deleted profile, and replacing it is correct.
  • The selected-incompatible unlocked state is visible for one render before the replacement applies. The note is honest for that render and is the only recovery path if the effect cannot run.
  • The AgentColumn function is near the 100-line lint cap once it branches on the new state. Extract the note into its own component.
  • Copy changes gate the build: check-i18n-keys.mjs fails on a missing key or an untranslated value in any locale.

Review remediation results

  • Merged the advanced base into the branch; the only conflict was the footer's reason table, resolved by keeping both new reason keys and evaluating the compatibility reason before the upstream dependencies-loading reason.
  • Codex review (P2): a workflow-locked incompatible agent with no other compatible profile fell through to the generic empty state. The state helper now takes the workflow lock and the selected row, keeps selected-incompatible for an enabled locked profile even when the compatible list is empty, and keeps a disabled locked profile on the empty state. AC 001.5 and the design derivation record the precedence.
  • Reverified: the nine dialog test files (137 tests), typecheck, eslint, i18n ratchet, prettier, and the specification linter.

Follow-up remediation

  • The guarded submit handler now applies the compatibility block to form, Ctrl/Cmd+Enter, and programmatic submissions. A focused setup test covers both selected-incompatible and none-compatible states.
  • The compatibility model now distinguishes selected-unavailable for an unlocked disabled or dynamic-off selection with a compatible alternative. The selector stays visible, submit stays disabled, and the autopick can replace the selection without showing a false empty state.
  • Added sentence-start and inline fallback locale keys, unavailable-state copy in all catalogs, and updated the requirements and system design.
  • Focused Vitest: 8 files, 136 tests passed. Typecheck, ESLint, i18n check and ratchet, E2E sleep ratchet, specification lint, architecture lint, and the production-build mobile E2E spec (2 tests) passed.