Commit Graph

872 Commits

Author SHA1 Message Date
José Almeida 7be7aede8b fix(codex): align with the standard SessionDirTemplate pattern
Codex was the only agent that bind-mounted its host home into the
container via an explicit `Mounts: [{home}/.codex → /root/.codex]`
entry, while every other ACP agent uses `SessionConfig.SessionDirTemplate`
+ `SessionDirTarget`. Move codex onto the same surface so the
host-state isolation work in the next commit covers it uniformly.

This change alone does not stop the host-state leak — the bind mount
still resolves to the host's ~/.codex for now — but it removes the
last agent-specific branch in the runtime config so the docker
container manager can apply one rule for all agents.
2026-05-06 21:03:31 +01:00
José Almeida ce49d6dfec feat(web): show "Resuming session" feedback in popover during reconnect
The bottom Resume button on the chat panel was leaving users staring at
nothing for the 5-30s reconnect window: prepare-progress events are
intentionally suppressed on resume (the workspace is already there), so
the popover sat in the idle phase even while session.state was STARTING.

Synthesize a "resuming" phase from session state when no prepare events
are flowing, rendered as a spinner row in the popover with copy
"Resuming session — Reconnecting to the existing environment…". The
button icon also flips to the spinner so the toolbar advertises the
in-flight work without the user having to open the popover.

Extract the prepare-status reducer + section component into their own
file so the main popover module stays under the 600-line ESLint cap.
2026-05-05 23:14:28 +01:00
José Almeida d35e601d42 test(docker): cover stop + resume → same container, branch preserved
Adds a Docker e2e test that exercises the user-visible stop-then-resume
flow: launch session, stop it via WS session.stop, launch a fresh
session on the same task, and assert the same task environment +
container ID are reused and the kandev feature branch is preserved.

Also nudge the executor popover padding: when the prepare-status
section is rendered above the Sprites/Docker info block, the cumulative
top+bottom paddings looked too generous around "Sprites sandbox" /
"Local Docker". Tighten both sections vertically without affecting the
horizontal rhythm or the bottom action row.
2026-05-05 23:05:22 +01:00
José Almeida b1b20b8cb7 fix(sprites): preserve sandbox on plain agent stop so resume is fast
StopInstance was calling sprite.Destroy() unconditionally, so every
session stop tore down the cloud sandbox. Resume then always fell into
the missing-sandbox fallback, re-cloned the repo, re-ran the prepare
script, and lost the agent's working tree. That's a 30s+ round-trip
for what should be "stop the process, then re-attach later".

Limit Destroy to terminal lifecycle reasons (task/session
deleted/archived), the same set runTerminalCleanupScript already uses.
Plain stops just close the proxy and leave the sandbox alive.

Also update the existing Docker e2e regex: the feature-branch suffix is
now a deterministic 6-hex-char prefix of the task UUID rather than a
random 3-char tail, so resume on legacy env rows lands on the same
branch.
2026-05-05 22:55:53 +01:00
José Almeida d9fc824edd test(sprites): lock in informative branch name in fallback warning
Now that BaseBranch is preserved on resume for clone-based executors,
the missing-sandbox fallback warning detail names the actual branch
("Cloning … on the same branch main") instead of the "(unknown)"
placeholder. Add a regression test so a future change to the resume
metadata propagation can't silently regress that copy.
2026-05-05 22:27:14 +01:00
José Almeida 5a126cdae0 fix(sprites,docker): prefer existing remote feature branch on resume
When a sandbox is destroyed and we provision a fresh one for the same
task, the agent may have previously committed and pushed onto the
feature branch. The previous prepare scripts only ran `git checkout -b
{{worktree.branch}}` which would overwrite that progress with a fresh
branch off the chosen base.

Try `git fetch origin <feature>` first; on success, `git checkout -B`
against the remote tip so the agent picks up where it left off. Falls
back to a fresh local branch when origin has nothing.

Also add the same checkout block to the Sprites prepare script — it was
missing entirely, so every Sprites session was committing onto main.
2026-05-05 22:27:04 +01:00
José Almeida 9012698172 feat(sprites,docker): generate a kandev feature branch for clone-based executors
Sprites tasks committed straight onto the chosen base branch because
SpritesPreparer didn't populate WorktreeBranch and the orchestrator's
"forward env.WorktreeBranch on reuse" check only fired for Docker.

Mirror the Docker preparer's TaskBranchName generation in Sprites,
extract a shared `nonWorktreeTaskBranch` helper that uses a
deterministic suffix derived from the task ID (so legacy env rows that
predate this change resolve to the same branch on every resume), and
let the orchestrator forward env.WorktreeBranch for any clone-based
executor — Docker or Sprites.
2026-05-05 22:26:42 +01:00
José Almeida 2c33997e1a fix(orchestrator): preserve base branch on resume for clone-based executors
Sprites and Docker resumes were sending the launch request with an empty
BaseBranch, so the in-sandbox prepare script's `git clone --branch <X>`
shifted positional args and failed with "repository '/workspace' does
not exist". applyResumeRepoConfig only set BaseBranch inside the
worktree-specific block; widen that to all executors that require a
clone URL.

Local executors are intentionally left untouched: LocalPreparer reads
BaseBranch and forces a `git fetch && git checkout` against the user's
own workspace, which would clobber the "use current state" path.
2026-05-05 22:26:22 +01:00
José Almeida c6b73a8a4d fix(docker): swap container glyph for a plain box icon
IconContainer's vertical ribbing reads as a microchip on small sizes.
IconBox is an unambiguous outlined cube. Use it for both docker
variants so the executor surfaces look like a box, not a chip.
2026-05-05 21:33:40 +01:00
José Almeida 4282ba262f feat(web): unify executor surfaces in the right-side popover
The desktop task top bar carried a small cloud icon (RemoteCloudTooltip)
in the breadcrumb cluster while the right-side Executor popover showed
the same kind of info — two surfaces for one concept. Remove the cloud
icon from the desktop top bar; kanban cards, task list rows, and the
mobile top bar keep theirs since they have no popover.

The popover button now picks its icon by executor type (cloud for
sprites, container for docker, server for local PC) using a shared
getExecutorStatusIcon helper. While the prepare progress is preparing
the icon swaps to a spinner so the user can see at a glance that work
is in flight.

The popover content gains a Preparing-environment section that streams
"Step N of M: <step>", an amber inline note when the missing-sandbox
fallback is in progress, an Environment-ready row with duration on
success, and a failed-step summary on failure. Polling promotes to the
active 3s cadence while preparing so the icon flips to ready without
the user opening the popover.
2026-05-05 21:20:05 +01:00
José Almeida 6609fe99d4 feat(web): friendlier header for sandbox-recovered prepare runs
When the only warning on a completed prepare is the missing-sandbox
fallback notice, render "Environment prepared on a fresh sandbox" in
the header instead of the generic "Environment prepared with warnings".
The fallback row still carries its own amber warning callout so the
user understands why a fresh sandbox was provisioned.
2026-05-05 21:19:47 +01:00
José Almeida 85885dd866 feat(sprites): provision a fresh sandbox when the persisted one is gone
Resuming a Sprites task whose sandbox was destroyed upstream returned
"sprite not found" and dead-ended the user. Detect the upstream 404 via
a typed ErrSpriteNotFound sentinel, mark the reconnect step skipped with
a user-facing warning explaining what happened, swap the prepare plan
in place to the fresh-provision shape, and continue the launch under a
new sandbox name on the same branch.

The progress plan is now mutable so the reporter can stream the fresh
steps live after the reconnect-then-fresh transition. Only the explicit
not-found case triggers fallback; other reconnect failures still
surface as before, and the fallback fires at most once per launch.
2026-05-05 21:19:39 +01:00
José Almeida 3790fc148f fix(docker): use container icon for local docker executor too
Local docker tasks were rendering the Docker brand "chip" mark while
remote docker uses the container glyph. Map both variants to
IconContainer so the executor surfaces look consistent.
2026-05-05 21:19:23 +01:00
José Almeida 4bdd418000 fix(sprites): reconnect sessions on existing sandboxes 2026-05-04 21:40:27 +01:00
José Almeida 4077eebbe9 fix(orchestrator): align environment reuse after rebase 2026-05-04 21:15:16 +01:00
José Almeida 001ecf9582 fix(docker): use container icon for docker tasks 2026-05-04 21:11:22 +01:00
José Almeida 2eed58f37c fix(docker): show task titles in container lists 2026-05-04 21:11:22 +01:00
José Almeida 3b07d72a91 fix(docker): stabilize container reuse and feedback 2026-05-04 21:11:22 +01:00
José Almeida c7149e90aa fix(docker): surface container launch progress 2026-05-04 21:10:32 +01:00
José Almeida 567a0414d6 fix(docker): explain missing agent credentials 2026-05-04 21:10:32 +01:00
José Almeida 3bc9e1bc02 fix(docker): track executor containers for cleanup 2026-05-04 21:10:32 +01:00
José Almeida 09057324b3 fix(docker): resume task environment after refresh 2026-05-04 21:10:32 +01:00
José Almeida 6c045f9a07 fix(docker): reuse task containers across sessions 2026-05-04 21:10:32 +01:00
José Almeida 379cbb29f2 fix(web): keep remote auth selection stable 2026-05-04 21:09:39 +01:00
José Almeida f890ff5432 fix(docker): connect agentctl through published ports 2026-05-04 21:09:39 +01:00
José Almeida 1f5a47c1b9 test(executor): cover applyExistingEnvironment executor-type mismatch guard 2026-05-04 21:09:39 +01:00
José Almeida 6cd50ab841 fix: address PR review feedback (CI, CodeRabbit, Greptile, Claude)
Backend:
- service_task_environments: fail closed when checkAnySessionRunning
  errors so a transient DB hiccup can't bypass the running-session
  guard and tear down a live container/sandbox; teardown is now truly
  best-effort with errors.Join across container/sandbox/worktree, so a
  failed Docker call doesn't permanently orphan the worktree on retry.
  Adds tests for the running-check error path and the multi-resource
  best-effort path.
- executor_execute: propagate GetTaskEnvironmentByTaskID errors instead
  of silently launching a fresh environment that orphans existing
  containers/worktrees; gate environment reuse on matching ExecutorType
  so a profile switch doesn't pass stale PreviousExecutionID/container
  IDs to the wrong backend.
- task_environment_handlers: reject malformed JSON on POST
  /tasks/:id/environment/reset (still allow empty body) so a destructive
  endpoint doesn't silently proceed with a bad payload.
- worktree (PushEnvironmentBranch): bound git push with a 30s context
  timeout and disable interactive prompts via GIT_TERMINAL_PROMPT=0
  so a stalled remote can't hang the request thread.
- manager_environment: explicitly call ensureClient() before
  ContainerMgr() in DestroyContainer/GetContainerLiveStatus so a missing
  Docker daemon surfaces as "docker unavailable" rather than the
  generic "container manager not initialized".
- container: WorkingDir always uses the in-container /workspace path
  rather than the host workspace path (Docker would happily start in an
  unrelated directory in host bind-mount mode); bootstrap now logs the
  prepare-script exit code so failures show up in container logs.
- docker/client: CreateContainerInteractive honors ContainerConfig.Entrypoint
  so the same config struct produces consistent behavior on both creation
  paths.

Frontend:
- executor-settings-button: extract useTaskEnvironment hook (component
  shrinks below the 100-line limit); only treat 404 as empty env (other
  errors no longer wipe a valid environment); track "loaded once" via a
  ref so loadEnv doesn't depend on env state — that dep was creating a
  new function identity on every successful poll, cancelling the
  3-second interval and turning the popover into a tight loop hammering
  /environment/live; copy-shell command uses sh instead of bash so it
  works on user-built minimal images.
- task-reset-env-confirm-dialog: AlertDialogAction onClick calls
  e.preventDefault() so Radix's auto-close doesn't dismiss the dialog
  before the async reset completes (loading spinner now visible; close
  is parent-controlled via onOpenChange).
- remote-credentials-card: track env method in selectedIds the same way
  files/gh_cli_token are tracked, and let initialChoice match either
  selectedIds OR envSecretId. First-time env-credential setup for an
  agent that exposes both files+env was broken: clicking the env radio
  never updated state, choice re-derived to "none", and the secret
  picker disappeared before the user could pick anything.
- e2e/executor-reuse: select the acknowledge checkbox by label, not by
  positional last(), so adding another checkbox to the dialog can't
  silently target the wrong control.

Format pass on prettier-flagged files; gofmt fixed two whitespace
nits in lifecycle files.
2026-05-04 21:09:39 +01:00
José Almeida ce79aac590 fix(build): always produce linux/amd64 agentctl as part of \make build\
Remote executors (local Docker, remote Docker, Sprites) bind-mount the
linux/amd64 agentctl binary into the container. On non-Linux hosts (e.g.
macOS dev installs) running plain \`make build\` produced only the host
binary, so the first Docker session would fail with:

  failed to build container config: agentctl linux binary not found

The top-level \`make dev\` already special-cased non-Linux hosts to chain
\`build-agentctl-linux\`, but \`make build\` and \`make start\` skipped it.
Add it as a dependency of the standard backend build so every install
ships a usable linux variant. Cross-compiling agentctl is fast (~1-2s)
and the binary is ~20 MB, so the cost is negligible.

Users who hit the error can run \`make build-backend\` (or \`make build\`
in apps/backend) to regenerate; setting KANDEV_AGENTCTL_LINUX_BINARY
remains a manual override.
2026-05-04 21:09:39 +01:00
José Almeida b1f57cec6b test(docker-e2e): add real-Docker Playwright project + first spec
A new Playwright project — \`docker\` — runs container-bound E2E specs
against a backend with KANDEV_E2E_DOCKER=1. The fixture probes for a
Docker daemon and skips the worker entirely when none is reachable, so
contributors without Docker can still run the rest of the suite.

The fixture builds a tiny kandev-agent:e2e image once per machine
(node:22-slim + git/curl), pre-seeds a local_docker executor profile,
and seeds a local repo with a file:// origin so the executor's
clone-inside-container path resolves without network access. A small
WS-based session.launch helper is added to the api-client for tests
that need to launch additional sessions on an existing task.

First spec — docker-launch.spec.ts — covers the smoke path: a session
launches in a real container, the task environment records its
container_id, and \`docker inspect\` confirms the container exists.
Two follow-up specs (multi-session env reuse + recovery from external
container removal) are wired up but marked test.fixme with comments
pinpointing the backend gaps that block them: persistTaskEnvironment
isn't called for workflow-prepared sessions (so task_environment_id
stays empty on those rows), and DockerExecutor.reconnectToContainer
loses the agentctl handshake auth token across launches (so reconnect
401s and falls back to a fresh container every time).

Run locally with: \`pnpm --filter @kandev/web exec playwright test
--config e2e/playwright.config.ts --project=docker\`.
2026-05-04 21:09:39 +01:00
José Almeida 2779fb24aa feat(docker): make mock-agent runnable inside Docker for E2E
Lay the backend groundwork so the existing mock-agent can run inside
local_docker containers — needed for real-Docker E2E tests that don't
require Anthropic/OpenAI API keys.

Container builder bind-mounts a host mock-agent binary at
/usr/local/bin/mock-agent when KANDEV_MOCK_AGENT_LINUX_BINARY is set
(or build/mock-agent-linux-amd64 sits next to the kandev binary), the
same pattern already used for agentctl. MockAgent.BuildCommand always
resolves via PATH so the same command works on host (E2E PATH includes
bin/) and in containers (mounted at /usr/local/bin/mock-agent).

Mock now declares non-nil empty RemoteAuth so it counts as a
remote-capable agent. The remoteauth catalog includes specs even with
zero methods, and the frontend gating helper treats an empty-method
spec as "no credentials needed" — letting mock-agent pass the
configure-on-executor check that hides uncatalogued agents on remote
executors.

repositoryCloneURL falls back to the local origin remote URL when the
repo has no provider info, so local-only repos with a real remote
(including file:// remotes used in E2E) work on remote executors that
clone inside the container.

Makefile: build-mock-agent-linux target mirrors build-agentctl-linux.
2026-05-04 21:09:39 +01:00
José Almeida cbfff9bc8a feat(web): gate agent profiles by executor profile credentials
Filter the agent profile dropdown in both the Create Task dialog and the
New Session dialog to only show agents that are actually configured for
the selected executor profile.

For local/worktree executors, no gating (agents need no extra creds).
For Docker/Sprites profiles, an agent is "configured" iff one of its
remote-auth methods has a value in `remote_credentials` or a non-null
secret in `remote_auth_secrets`. Agents without an entry in the remote
auth catalog (Copilot, Amp, OpenCode, TUI, mock) cannot carry creds on
remote executors and are blocked.

Spec IDs match `AgentProfileOption.agent_name` (the registry-type string
like "claude-acp"); `agent_id` is a DB UUID and unrelated to the catalog.

The Create Task footer surfaces a "No agent profile is configured for X"
tooltip and disables submit when the selected executor profile has no
compatible agents. The New Session dialog auto-swaps to the first
compatible agent and shows the same banner inline.

`useRemoteAuthSpecs` returns a `loaded` flag so callers don't filter
during the initial fetch (would otherwise hide everything).
2026-05-04 21:09:39 +01:00
José Almeida f169d25b72 fix(executor-profile): live Configured badge + session/load resilience + live container status
Three independent fixes that all surface together while iterating on the
Docker executor flow:

- Remote Credentials card: the per-agent "Configured" / "Not Configured"
  badge was driven by useState that only seeded once on mount. Picking a
  secret in the dropdown updated the parent's `agentEnvVars` map but the
  local `choice` never moved to `"env"`, so the badge stayed grey until
  the page was reloaded. Derive `choice` straight from props every render
  so the badge tracks live state.

- ACP session/load: when the agent process restarted between turns
  (common with claude-agent-acp because its session map is in-memory),
  the host's session/load call failed with JSON-RPC -32002 "Resource not
  found" and the entire launch errored out. Treat that as the same kind
  of "session is gone" signal as -32601 / capability=false and fall back
  to session/new. Worst case the agent loses prior context; best case
  the user just keeps typing.

- Executor Settings popover: dropped Execution ID and Environment ID
  rows (debug-only noise users don't need), kept Container ID + the
  copyable `docker exec -it … bash` line, added a live status badge
  with tones for running / exited / paused / dead / error / missing,
  and a 3-second polling interval so the badge follows the real
  container state. New `GET /api/v1/tasks/:id/environment/live`
  endpoint plumbs Docker `inspect` data through the lifecycle manager
  → task service → handler. ContainerLiveStatus mirror type at the
  service layer keeps lifecycle types out of the public surface.
2026-05-04 21:08:51 +01:00
José Almeida e9081b3119 fix(executor-profile): persist remote auth secrets/credentials for Docker profiles
The Remote Credentials card (per-agent secret picker, plus the GitHub
credential checkbox) is rendered for any remote executor — Sprites,
local_docker, or remote_docker — but the save path in both the create
and edit pages only emitted `remote_auth_secrets` and `remote_credentials`
when `isSprites` was true. Selecting a secret for the Claude / Codex /
Auggie ANTHROPIC_API_KEY-style entry on a Docker profile updated form
state, the dropdown showed it as picked, but Save Changes silently
dropped the value before posting to the API. On reload the agent showed
as "Not configured", and any session start failed with
`{"code":-32000,"message":"Authentication required"}`.

Switch the guards to `isRemote` so the same data persists for all remote
profile types. The backend orchestrator already propagates
`profile.config.remote_auth_secrets` and `remote_credentials` through to
metadata for any containerized executor and resolves them via
`applyContainerCredentials` → `resolveAuthSecrets`, so no backend change
is needed once the values actually land in the saved profile.
2026-05-04 21:08:51 +01:00
José Almeida 2995270aa0 fix(docker): patient health check + resilient bootstrap
Two follow-ups to the Docker bootstrap work:

- The 30 retries × 500ms = 15s budget for agentctl health was too tight.
  In the previous flow agentctl was a separate exec from the prepare
  script, so it came up immediately. Now it runs after the prepare
  script (which does git clone, optional installs), so first launch can
  easily exceed 15s and produce a misleading "agentctl not healthy after
  30 retries" error. Bumped to 240 retries (120s) and surfaced the last
  underlying error in the message so failures point to the real cause.

- Prepare scripts commonly start with `set -e`/`set -eu`; running them
  via `eval` propagated the option to the outer bootstrap shell, so any
  failure killed the bootstrap before `exec /usr/local/bin/agentctl`.
  Run prepare in a subshell so its `set -e` is contained, and start
  agentctl regardless. If prepare fails, agentctl still comes up — the
  host can connect, surface the failure, and let the user debug from
  the Executor Settings popover.
2026-05-04 21:08:51 +01:00
José Almeida 50ba29fb30 feat(docker): self-contained Dockerfile + mount agentctl + capabilities adapter
Make the Docker executor work with a Dockerfile users can actually build,
without depending on a maintainer-shipped image:

- The container now launches a small bootstrap (`sh -c "$KANDEV_PREPARE_SCRIPT
  ; exec /usr/local/bin/agentctl"`) regardless of the image's ENTRYPOINT.
  The agent CLI is no longer wired through container Cmd; agentctl receives
  it later via its CreateInstance HTTP API.
- Host agentctl-linux binary is mounted into the container at
  /usr/local/bin/agentctl via the existing AgentctlResolver. User-built
  images don't have to bake it in.
- Default Dockerfile in the executor profile UI replaced with a working
  node:22-slim + git + curl image — runs npx-based ACP agents (Claude,
  Codex, OpenCode, Auggie) out of the box.
- lifecycleAdapter now exposes RequiresCloneURL / ShouldApplyPreferredShell
  so the orchestrator can correctly identify containerized executors and
  resolve the clone URL. Without this, applyRepositoryConfig short-circuits
  on `e.capabilities == nil`, the cloneURL is never resolved, and the
  prepare script fails with `repository '{{repository.clone_url}}' does
  not exist`.
- applyRepositoryConfig also surfaces the resolved clone URL to script
  metadata so the prepare script can interpolate {{repository.clone_url}}.
- docker.ContainerConfig gains an Entrypoint field, plumbed to the docker
  client.

Verified end-to-end on a local stack with the new minimal image: the
container starts, bootstrap clones the GitHub repo, agentctl launches and
hands off to claude-acp inside the container.
2026-05-04 21:08:51 +01:00
José Almeida 8cbeb32b7b test(docker): cover container config build + Docker profile persistence
Adds two regression tests for the recent Docker fixes:

- container_config_test.go (Go unit): asserts buildContainerConfig
  expands `{workspace}` in WorkingDir (defaulting to /workspace when no
  host WorkspacePath is set) and that ContainerConfig.ImageTagOverride
  takes precedence over the agent runtime's hardcoded `rt.Image`. No
  Docker daemon required.

- docker-profile-persistence.spec.ts (Playwright E2E): creates a
  local_docker executor + profile, opens the profile edit page, clicks
  "Use defaults" to populate the Image Tag input + Dockerfile editor,
  hits Save, then refetches the profile via API and reloads the page to
  verify both fields round-trip through `profile.config`. Catches the
  prior regression where DockerSections owned the form state locally
  and never wrote it back. Runs against the existing E2E backend that
  has KANDEV_DOCKER_ENABLED=false — we only test the persistence path,
  not container creation.

Adds an api-client helper (`getExecutorProfile`) for the round-trip
assertion.
2026-05-04 21:08:51 +01:00
José Almeida f470ea85e2 fix(docker-profile): persist Dockerfile/image_tag and apply tag override at runtime
Two bugs:

1. The Dockerfile editor and Image Tag input on the executor profile page
   stored their values in component-local useState inside DockerSections,
   not in the form state. Saving the profile never wrote them back, so
   "Use defaults" / typing in the editor / changing the tag never
   persisted — and the Build button was the only thing that consumed
   them. The same gap existed in the new-executor flow's
   buildProfileConfig.

   Lift `dockerfile` and `image_tag` into useProfileFormState (edit) and
   useCreateProfileFormState (new), and emit them in buildSaveConfig /
   buildProfileConfig so they round-trip through profile.config.

2. Even when an image_tag is set on the profile, the container builder
   ignored it. imageName was always derived from the agent runtime's
   hardcoded `kandev/multi-agent`, so a user who built a custom image
   like `kandev/agent:latest` would still see Docker fail with
   "No such image: kandev/multi-agent:latest".

   Propagate `profile.config.image_tag` → metadata
   (`image_tag_override`) → ExecutorCreateRequest → ContainerConfig,
   and let it override `rt.Image` in buildContainerConfig when set.

Also align the frontend default image tag (kandev/multi-agent:latest)
with the backend's hardcoded runtime default so users who don't override
end up with a working build.
2026-05-04 21:08:51 +01:00
José Almeida 6cf0854e3b fix(docker): expand {workspace} placeholder in container WorkingDir
The agent runtime config carries `WorkingDir: "{workspace}"` for most ACP
agents. Mount sources were already expanded via expandMountSource, but
WorkingDir was passed through verbatim, so Docker rejected new containers
with:

  Error response from daemon: the working directory '{workspace}' is
  invalid, it needs to be an absolute path

Run the same expansion on WorkingDir, defaulting to /workspace when no
host WorkspacePath is set (the clone-inside-container case introduced by
PR #654).
2026-05-04 21:08:51 +01:00
José Almeida 4228f92aef feat(web): add executor settings popover with reset environment action
A new button on the task top bar opens a popover showing the active
TaskEnvironment — executor type, status, container/sprite/worktree
identifiers, and a copy-ready 'docker exec' command for local Docker.
The popover hosts the destructive Reset Environment action so users have
one obvious place to inspect and manage the environment without
cluttering the New Session dialog.

The reset confirmation dialog requires explicit acknowledgement that
uncommitted changes will be lost. When the environment has a host-side
worktree it offers an opt-in 'push branch before resetting' checkbox so
committed work can be pushed first; the backend aborts the reset on
push failure.

- task-environment-api.ts: fetchTaskEnvironment + resetTaskEnvironment
- executor-settings-button.tsx: popover trigger + identity panel
- task-reset-env-confirm-dialog.tsx: destructive confirm with push opt-in
- task-top-bar.tsx: wire the new button into the right-side controls
- e2e: replace fresh-env-toggle test with reset-from-popover flow
2026-05-04 21:08:51 +01:00
José Almeida 9b82595cfd feat(backend): add task-level reset environment action
Users sometimes need a clean slate — broken packages, stale caches, a
container that drifted into a bad state. Reset Environment is the explicit
escape hatch: tear down the recorded container, sandbox, and/or worktree
for a task and delete the TaskEnvironment row so the next session starts
fresh.

- service.ResetTaskEnvironment(taskID, ResetOptions) is the entry point.
  ResetOptions.PushBranch optionally git-pushes the workspace branch
  before teardown so committed work isn't dropped on the floor; a failed
  push aborts the reset and leaves the environment intact.
- A new EnvironmentDestroyer interface in the task service hides the
  per-resource teardown behind a single seam, implemented in cmd/kandev
  by delegating to lifecycle.Manager (containers, sprites) and
  worktree.Manager (worktrees, branch preserved).
- lifecycle.Manager.DestroyContainer / DestroySandbox dispatch to the
  Docker and Sprites executor backends without leaking model types into
  the lifecycle package.
- Reset is blocked while any session on the task still has an
  ExecutorRunning row (returns ErrSessionRunning → HTTP 409). Per-resource
  teardown is best-effort but the DB row is only removed once everything
  is confirmed gone or already missing, so a partial failure is retryable
  by clicking Reset again.
- Endpoint: POST /api/v1/tasks/:id/environment/reset.
2026-05-04 21:08:51 +01:00
José Almeida 154b2a39fb feat(orchestrator): generalize task environment reuse across executor types
Worktree, container, and sandbox IDs now flow uniformly from a persisted
TaskEnvironment back into LaunchAgentRequest through a single helper. This
gives Sprites session-to-session reuse the same first-class treatment that
Docker already gained from the GitHub-token-injection PR, and lets a fallback
launch refresh the environment row instead of leaving stale IDs in the DB.

- applyExistingEnvironment propagates WorktreeID, ContainerID (via metadata),
  and SandboxID (as sprite_name metadata) onto the launch request
- persistTaskEnvironment refreshes those IDs when the executor ends up
  creating new resources (e.g. the previous container was pruned)
- extractSandboxID lifts the sprite identifier out of executor response
  metadata so it can be persisted on the TaskEnvironment row
2026-05-04 21:08:51 +01:00
José Almeida a4e0fc0452 feat: attribute cross-task agent messages with a sender badge (#819)
* feat(orchestrator): carry sender_task metadata through user message paths

Add WithSenderTask builder to UserMessageMeta and a metadata-aware
QueueMessageWithMetadata variant on the message queue. The drain path
in event_handlers_agent merges queued metadata into the recorded user
message so attribution survives the queue → DB transition.

* feat(mcp): attribute message_task_kandev calls to the sending agent

The MCP server inside agentctl now injects sender_task_id and
sender_session_id from its own server context. The backend handler
validates the sender (rejecting missing IDs, self-messages, and unknown
tasks), looks up the sender title, wraps the prompt in a <kandev-system>
attribution block, and propagates structured metadata across all three
delivery paths (queue, created-session, record+prompt). The wrapped
content lets the receiving agent identify the peer both live and on ACP
session resume; the existing sysprompt strip pipeline hides the wrapper
from the UI.

* feat(web): render sender-task badge on cross-task agent messages

User messages whose metadata carries sender_task_id now render a
clickable purple badge above the bubble, linking to the source task.
The badge live-resolves the sender's title from the kanban store and
falls back to a greyed snapshot when the task isn't loaded
(cross-workspace, deleted, etc.). Adds a non-subscribing useTaskById
selector so badges don't fan out WS subscriptions per message.

* test(e2e): cover cross-task agent message attribution

Eight scenarios driven through the mock-agent's MCP script directive:
queue path, prompt path, self-message rejection, unknown-target error,
badge link target, live title re-resolution after rename, kandev-system
wrapper preservation in raw_content, and prompt bodies containing their
own kandev-system tags. Adds an updateTaskTitle helper to ApiClient.

* fix: address CodeRabbit + Greptile P1 findings on PR #819

- requeueMessage now forwards queuedMsg.Metadata via QueueMessageWithMetadata
  so sender attribution survives transient delivery failures and retries.
- CREATED-session branch in handleMessageTask records the user message AFTER
  StartCreatedSession succeeds, so a launch failure no longer leaves an
  orphaned attributed chat row.
- SenderTaskBadge: collapse identical displayTitle/fullTitle locals.
- E2E: extract waitForTargetIdle helper; replace the 1.5s raw sleep in the
  collision-check test with the same poll the rest of the suite uses.

* fix: address Claude review suggestions on PR #819

- wrapAgentMessage strips </kandev-system> from the embedded sender title
  before formatting. Without this, a title containing the closing tag
  would short-circuit the non-greedy strip regex and leak the attribution
  tail into the visible UI bubble. Metadata snapshot keeps the original
  title so the badge still displays it.
- Add a regression test covering the malicious-title case.
- Document the deliberate cross-workspace messaging policy on the sender
  lookup so future readers don't add a workspace-scope check (Q13 of the
  feature plan locked this as intentional).
2026-05-04 19:30:10 +01:00
Carlos Florêncio cb0feea650 fix(web): dedup topbar PR button against auto-shown PR panel (#812) 2026-05-04 19:20:37 +01:00
Carlos Florêncio c042050469 fix: stabilize task switch flow and session recovery (#818) 2026-05-04 18:53:33 +01:00
Carlos Florêncio 39b9369ae1 fix: suppress spurious task-failed toasts on resume and reconnect (#814) 2026-05-04 17:58:59 +01:00
João Salavisa 5c5e7bf6a7 fix(changes-panel): scope commits/diff to live branch divergence; flatten single-repo (#816)
* fix(changes-panel): scope commits/diff to live branch divergence; flatten single-repo

Two bugs in the COMMITS panel made it surface commits and file changes
that were already on main:

1. The commit list and the cumulative diff both anchored to a stale base.
   Commit list used `git merge-base HEAD <local-branch>` and the file diff
   used the session's stored base SHA captured once at session start. On
   long-lived worktrees the local main ref lags far behind origin/main, so
   the merge-base lands on an old SHA; once the agent merges main back in,
   the stored base is also stale. Both end up sweeping in dozens of main
   commits as if they were the branch's work.

2. `git log <base>..HEAD` follows both parents of a merge commit by
   default, surfacing main commits brought in via merges from main.

Fixes:
- agentctl computes merge-base against `origin/<target_branch>` (with a
  local-ref fallback for unfetched/local-only branches), and uses that
  for both the commit log and the cumulative diff. Mirrors GitHub's
  three-dot PR diff semantics — anchor to live divergence with the
  upstream, not a session-start snapshot.
- `git log` now passes `--first-parent` so only the branch's own line of
  work plus its merge commits show up; commits brought in via merges from
  main are excluded.

Frontend: drop the per-repo "REPOSITORY" sub-header in the COMMITS panel
for single-repo workspaces. CommitsRepoGroup gains a `showHeader` prop;
when the workspace has one untagged group the section renders flat with
the per-repo Push / PR (and Stage all / Commit / Unstage all) buttons
lifted into the section header. Multi-repo rendering unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address PR review feedback

- Scope --first-parent to the divergence-range path only (Greptile P1).
  The flag is wrong for open-ended history calls — future activity
  widgets etc. need the full graph.
- Log empty merge-base case in cumulative diff (Greptile P2). Silent
  fallback to stored base was invisible during diagnostics.
- Rename TestRunGitCumulativeDiffForRepo_PrefersOriginRef →
  TestComputeMergeBase_CorrectAnchorForCumulativeDiff (Claude). Test
  exercises computeMergeBase, not the runGitCumulativeDiffForRepo
  conditional; new name reflects what it actually proves.
- Add TestGetLog_NoBaseReturnsFullGraph to lock in the --first-parent
  scoping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address PR review round 2

- Drop dead ActionButtonsSection from changes-panel-timeline.tsx (unused
  export, no consumers anywhere). Brings the file from 640 → 516 lines,
  comfortably under the 600-line TS limit Claude flagged.
- Extract isSingleRepoGroup helper into lib/group-by-repo.ts and use it
  in all three sites (CommitsSection, FileListBody, FileListSection) so
  single-repo detection has one source of truth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:28:04 +01:00
João Salavisa b30933240b fix: sanitize multi-repo worktree dirs and polish task-create chip dropdown (#815)
* fix: sanitize multi-repo worktree dirs and polish task-create chip dropdown

Backend: org-prefixed Repository.Name like "owner/repo" was used verbatim
as the worktree subdirectory under the task root, nesting one level too
deep and breaking agentctl's sibling-based multi-repo detection in
scanRepositorySubdirs. Sanitize to a single path-safe segment so all
worktrees land as siblings of the task root.

Web: the multi-repo chip dropdown built its own option list and dropped
the path Badge plus search filtering that the legacy header dropdown
shipped. Restore parity (name + path), widen the popover (288 → 360 px),
stack name and full path on two lines so neither has to truncate
aggressively, hide the Badge for workspace repos without a local clone,
and use a path-aware scorer that floors weak fuzzy matches so unrelated
paths stop surfacing for short queries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address PR review feedback

- Add ErrInvalidRepoName sentinel and use it in createInTaskDir when
  RepoName sanitizes to empty, replacing the misleading reuse of
  ErrTaskDirRequired (Claude/Greptile).
- Reuse repoDirHyphenRun in SanitizeForBranch instead of recompiling the
  regexp per call (CodeRabbit nitpick).
- Document the SanitizeRepoDirName collision edge case in the doc
  comment (Greptile).
- Extract scoreRepo to lib/utils/repo-filter.ts and add unit tests
  pinning the 0.4 fuzzy-match threshold (Claude).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:22:28 +01:00
João Salavisa 96c68a0af9 fix(backend): return mcp message_task once dispatched, not after target turn ends (#817)
* fix(backend): return mcp message_task once dispatched, not after target turn ends

The MCP message_task_kandev tool was synchronously waiting for the target
session's entire turn to complete before returning, even though its
description advertises a fast dispatch status. For idle target sessions,
the call would hang for the full duration of the agent's response (with
no upper bound — waitForPromptDone has no timeout).

Threads a dispatchOnly flag from the MCP handler down through
PromptTask → executor.Prompt → PromptAgent → SendPrompt. When set,
SendPrompt returns once agentctl.Prompt accepts the prompt instead of
blocking on the agent's complete event. Stale completion signals from
prior dispatch-only prompts are drained at the start of SendPrompt so
they don't poison the next waiting call.

* fix: address PR review feedback

- Replace time.Sleep with channel-based wait for WS connection in new
  SendPrompt tests, per CLAUDE.md (channel sync over time.Sleep for
  non-subprocess tests).
- Drop redundant Warn that logged the remapped sentinel after the
  original error was already logged by the preceding Warn.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-04 17:17:45 +01:00
Carlos Florêncio 22c8baf015 fix(backend): correct task_environments migration order for older DBs (#811) 2026-05-04 09:34:17 +01:00
Carlos Florêncio b01e6abc06 fix: retry CLI passthrough launch without resume flag after fast-fail (#810) 2026-05-04 08:45:20 +01:00