Commit Graph

59 Commits

Author SHA1 Message Date
João Salavisa 97c1f689e4 feat(workflow): explicit step-completion signal for auto-advance (#1276)
* feat(workflow): explicit step-completion signal for auto-advance

Adds `step_complete_kandev` MCP tool + `WorkflowStep.auto_advance_requires_signal`
column so workflow steps can gate their `on_turn_complete` transitions on an
agent-emitted completion signal instead of inferring "done" from any turn-end.
See docs/decisions/0015-explicit-completion-signal-for-auto-advance.md for the
motivation, alternatives considered, and trade-offs.

* fix(workflow): address PR review feedback for explicit completion signal

- Dedup `LoadPendingStepSignal` into `internal/task/models` so the MCP
  handler (write site) and the orchestrator (read site) share one decoder.
- Gate the out-of-band subscriber on `WorkflowStep.AutoAdvanceRequiresSignal`
  so `step_complete_kandev` is not an unintended manual-advance path for
  signalless steps.
- Fail closed on step-load errors in `processOnTurnCompleteViaEngine`: a
  loader hiccup must block the transition rather than silently bypass the
  gate.
- Reject `step_complete_kandev` for terminal sessions
  (Completed / Failed / Cancelled) so the agent gets a clear error instead
  of `accepted: true` followed by a no-op.
- Surface bus-publish failures back to the agent instead of swallowing
  them — the idempotency guard makes the retry safe and prevents the
  session from getting stuck.
- New `TestOnStepCompletionSignaled` covers the subscriber paths
  (RUNNING / WAITING+match / stale step / not signal-gated).
- ADR doc fixes: payload field list now matches the wire shape, writer
  ownership is documented, the unsubstantiated 250ms-debounce claim is
  removed, and the late-signal re-close race is recorded as a deferred
  open question.
- Drop the new toggle's HelpTip in favour of an inline description — the
  shadcn tooltip portal was intercepting clicks on the agent-profile
  dropdown options in the workflow settings E2E.

* fix(workflow): round-2 review fixes for explicit completion signal

- Split `handleStepComplete` into `resolveStepCompleteTarget` +
  `publishStepCompletionEvent` helpers so the entrypoint clears funlen
  (88 → under 80 lines) and the two halves are independently testable.
- Classify session/task load errors instead of returning `NotFound` for
  every failure: use the exported `taskrepo.ErrTaskNotFound` sentinel for
  task lookups, substring-match for the session repo (no sentinel today),
  and surface anything else as `InternalError` with the raw error logged
  but not leaked to the agent — so a transient DB hiccup triggers a retry
  rather than session abandonment.
- Pin `step_complete_kandev` explicitly in the mode-specific tool-count
  tests: `Contains` in `ModeTask`, `NotContains` in `ModeConfig` and
  `ModeOffice`, so accidental dereg or cross-mode registration is caught
  by the diff instead of by a still-equal count.

* fix(workflow): round-3 review fixes + defer pipeline-editor toggle

- Defer the "Wait for agent completion signal" sub-toggle to a follow-up
  PR. Adding it to the step config card shifts the layout enough that the
  agent profile select's Radix popper opens into a sibling tooltip's hover
  path, breaking workflow-agent-profile.spec.ts on shard 7. The backend
  column ships in this PR — workflows can opt in via the YAML import or
  the MCP `update_workflow_step_kandev` tool — and the UI affordance gets
  its own iteration where the test interference can be investigated
  without blocking the rest of the change.
- Re-publish the bus event on the `already_signaled` short-circuit when
  the session is WAITING_FOR_INPUT. Without this, an agent's retry after
  the first call's publish failed would short-circuit on the persisted
  bag, never fire the subscriber, and leave the session stuck.
- handleStepComplete docstring: drop the non-existent feature-flag claim
  and the "audit entry" wording — gating is purely
  `WorkflowStep.AutoAdvanceRequiresSignal` and the bag is cleared on the
  next turn start with no separate audit trail.
- `step_complete_kandev` tool description: same fix — clarify that the
  call succeeds for non-gated steps but the signal is discarded on the
  next turn start (no audit history to query later).

* test(workflow): cover handleStepComplete + reword defer-comment

- Add `TestHandleStepComplete_*` (6 subtests, all passing) covering the
  uncovered handler paths flagged by Claude/cubic on round 3:
  missing-fields validation, session-does-not-belong-to-task ownership
  guard, terminal-session rejection (Completed/Failed/Cancelled),
  first-call happy path (bag persisted + bus payload shape pinned), dedup
  while RUNNING (no re-publish), and dedup while WAITING_FOR_INPUT (the
  retry-after-publish-failure path — must re-publish so the subscriber
  fires).
- Reword the deferred-toggle comment to drop the YAML-export claim:
  the workflow export model doesn't yet serialize
  `auto_advance_requires_signal`, so MCP `update_workflow_step_kandev`
  is the only configuration path that round-trips today. Note the
  follow-up needs to extend the export schema alongside the UI toggle.

* test(workflow): pin signaled_at + drop redundant GetTaskSession on bag-clear

- Assert `signaled_at` is present and parses as RFC3339Nano in
  `TestHandleStepComplete_FirstCallAccepted`. The field is part of the
  handler's documented response contract; pinning it catches accidental
  drop/rename regressions.
- Add `clearPendingStepSignalByID(ctx, sessionID)` as a thin DB-only
  variant of `clearPendingStepSignal` and use it from
  `executeStepTransition`, where the session struct isn't read after the
  clear. Avoids an extra `GetTaskSession` round-trip per transition.
  `clearPendingStepSignal(session)` still exists for the
  in-memory-mutation case (subscriber's stale-step path).

* fix(workflow): subscriber stale-clear guard + ADR/comment drift fixes

- onStepCompletionSignaled's stale-step path now reads the bag and only
  clears it when its current StepID matches the (stale) event's stepID.
  Previously the unconditional clear could destroy a valid signal that
  the new step's agent had just written when sessions are reused across
  steps without auto_start_agent. (Greptile P1.)
- Drop the "feature flag" phrase from WorkflowStep.AutoAdvanceRequiresSignal
  docstring — leftover from an earlier draft. The gating is purely the
  boolean field; the global flag was removed two rounds ago.
- Merge the two adjacent `if triggerOnEnter` blocks in
  executeStepTransition so the full triggerOnEnter=true path reads top-
  to-bottom in one place. (CodeRabbit + cubic.)
- ADR 0015 Mechanism section: re-label the SessionStepHistory.Metadata
  audit annotation, the `step_complete_signaled` system message, and the
  expvar telemetry counters as deferred. They're planned shapes, not
  current behaviour — calling them "Audit" / "Chat visibility" /
  "Telemetry" in present tense was misleading for future readers.

* fix(workflow): drop subscriber complexity + reload session on stale-clear

- Extract `parseStepCompletionEvent` and `stepIsSignalGated` helpers from
  `onStepCompletionSignaled`. Cyclomatic complexity goes 17 → under 15
  (cyclop on new code, CI lint regression after the round-6
  stale-clear guard).
- The stale-step branch now re-loads the session from the DB before
  deciding whether to clear the bag. The previous round-6 fix checked
  the local `session.Metadata` snapshot, which can be older than a
  concurrent write from the new step's agent — that check could see a
  stale signal that had since been overwritten with a valid one and
  erase the new bag entry. The narrower load-on-clear is cheap and
  closes the cubic P1 hole. (The broader race in
  `processOnTurnCompleteViaEngine` reading session.Metadata is the
  monotonic-marker open question in ADR 0015; out of scope here.)

* test(workflow): pin stale-event guard's preserve-current-bag side + dedup StringFromAny

- Add `TestOnStepCompletionSignaled` subtest "stale event → valid bag
  for CURRENT step is preserved" — fires a stale step-A event while
  the bag holds a valid step-B signal, asserts the bag still contains
  StepID=step_b after the subscriber runs. Pins the negative side of
  the round-6 `existing.StepID == stepID` guard, so a regression that
  drops or inverts the guard (and would silently leave signal-gated
  steps stuck) fails the test instead of slipping through.
- Promote `pendingSignalString` to exported `models.StringFromAny`
  and reuse it from the orchestrator subscriber's
  `parseStepCompletionEvent`. Eliminates the byte-for-byte duplicate
  helper across the two packages; the bag-shape decoder and the bus-
  event decoder now share one source of truth for "missing string".

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-05 18:32:27 +01:00
Nicolas Lenepveu 08189ec539 feat(integrations): add Sentry integration with issue watcher (#1133) 2026-06-04 10:18:17 +01:00
Carlos Florêncio dfc132c6f1 fix(orchestrator): block workflow advance during pending clarifications (#1251) 2026-06-02 23:38:41 +01:00
João Salavisa dfaab11959 feat: full GitLab integration — parity with GitHub (#1120)
* feat(backend): full GitLab parity — store, service, poller, WS, presets

Add the watcher/poll/write-action surface on top of the existing GitLab
client + service skeleton:

* 5 new SQLite tables (mr_watches, review_watches, review_mr_tasks,
  issue_watches, issue_watch_tasks, action_presets) + CRUD
* Client + Service: MergeMR, GetProjectMergeMethods, GetProtectedBranch,
  ListUserProjects, SearchProjects, SetMRLabels, SetMRAssignees
* Service: review/issue/MR watch CRUD + Check/Trigger, GetStats,
  CleanupAllReview/IssueTasks (cleanup-policy aware), TriggerMRSync,
  action-presets CRUD with defaults
* Background poller (MR / review / issue loops) wired from main.go
* ~35 GitLab WS action constants + handlers.go registrations
* Mock controller for E2E seeding (MRs, issues, pipelines, discussions,
  approvals, branches)
* eventBus + taskDeleter + taskSessionChecker plumbed through Service
* Event types: GitLabMRFeedback / NewReviewMR / NewIssue / TaskMRUpdated

* feat(web): GitLab frontend parity — types, API, store slice, domain hooks

Extend the frontend GitLab integration on top of the new backend surface:

* HTTP API client expansion (gitlab-api.ts): full CRUD for review/issue/MR
  watches, MR write actions (merge/approve/labels/assignees), MR
  files/commits/feedback, action presets, projects autocomplete, stats
* Store slice expansion: review/issue/MR watches lists, action presets
  by workspace, stats + status with loading flags. Properties prefixed
  with gitlab to avoid collision with GitHub slice
* Domain hooks: use-gitlab-status, use-gitlab-stats,
  use-gitlab-review-watches, use-gitlab-issue-watches,
  use-gitlab-action-presets — mirror github hook shapes
* Type model: ReviewWatch, IssueWatch, MRWatch, ActionPresets, Stats,
  ProjectMergeMethods, MRApproval, Pipeline, MRFeedback, etc.
* Backend controller HTTP routes for watches/presets/write-actions/
  projects/stats so the frontend HTTP API client has endpoints to hit

All passes: backend (build + vet + lint + tests), web (typecheck + lint +
2400 tests).

* fix(gitlab): wire orchestrator event handlers, fix label-filter drop, add tests

Address Claude review blockers on the parity PR:

1. Wire orchestrator subscriptions for GitLab review/issue watch events.
   Add event_handlers_gitlab.go with handleGitLabNewReviewMR /
   handleGitLabNewIssue, dedup reservation handshake, and task-creator
   interfaces (mirrors event_handlers_github.go). main.go now calls
   orchestratorSvc.SetGitLabService so the dedup APIs are usable
2. Fix silent label-filter drop: when a custom_query is set on an issue
   watch, labels were being appended to the unused filter arg. Fold
   labels into customQuery (or default filter) so they actually reach
   GitLab; extracted appendLabelsToQuery helper with explicit handling
   of pre-existing labels= clauses
3. Switch event payloads to pointer types (mirrors GitHub) so
   handlers can type-assert pointer events
4. Backend tests: store_watches_test.go (CRUD + reserve/assign for all
   4 new tables + presets), service_watches_test.go (label-filter
   helper, cleanup policy, project normalisation), action_presets_test.go
5. Frontend slice tests: review/issue watch CRUD round-trips, action
   presets + stats reducer coverage

All green: backend (build+vet+lint+tests), web (typecheck+lint+2403 tests).

* fix(gitlab): address remaining blockers — cleanup wiring, safe defaults, nil guards

Address the second round of Claude review blockers:

1. main.go: wire SetTaskDeleter + SetTaskSessionChecker on services.GitLab
   (mirrors GitHub). Without these, manual cleanup sweeps would always
   error with "task deleter not configured"
2. service_cleanup.go: default cleanup policy on transient DB error is now
   CleanupPolicyNever (preserve tasks) instead of CleanupPolicyAuto
   (silently delete). Genuine "watch was deleted" path also falls under
   the safe-side default; user can manually delete the orphan tasks
3. service_watches.go: add nil guards on requireStore() so the 6 MR-watch
   list/get/delete methods return an errStoreUnavailable error instead
   of panicking when the SQLite store failed to construct at boot
4. use-gitlab-status.ts + use-gitlab-stats.ts: add per-mount attemptedRef
   so an unreachable GitLab doesn't trigger an infinite re-fetch loop —
   the previous useEffect re-ran every render because the failure path
   left status null, satisfying the !status guard

* fix(gitlab): propagate session-check errors, clamp poll interval on update

Address remaining Claude blockers:

* service_cleanup.go: HasUserAuthoredMessage transient errors are now
  preserved (return false → skip delete) instead of silently ignored;
  the alternative was occasionally deleting tasks a user had touched
* service_watches.go: extract clampPollInterval helper applying the same
  bounds (0 → default, <60 → 60) as the create path; both
  applyReviewWatchPatch and applyIssueWatchPatch use it so user-supplied
  zero or tiny values via UpdateXxxWatchRequest no longer hammer GitLab

* fix(gitlab): clear dupl lint, fix preset retry loop + per-workspace watch cache

* service_watches.go: applyReviewWatchPatch / applyIssueWatchPatch get
  nolint:dupl markers — they share shape but per-domain field validation
  lives in the create paths, so deduplicating via generics would obscure
  the contract. Restores backend lint to 0 issues
* use-gitlab-action-presets.ts: per-workspace attemptedRef set so a
  failing preset fetch doesn't retry on every render
* use-gitlab-review-watches.ts + use-gitlab-issue-watches.ts: track
  lastFetchedRef per consumer so a workspace switch triggers a refetch.
  The slice-level loaded flag is shared across instances and can't
  double as a per-workspace cache key

* fix(gitlab): add nil-store guards across review/issue watch + cleanup paths

Round-3 Claude finding: ReviewWatch / IssueWatch / preset / reservation
methods still called s.requireStore().X() directly. If NewStore fails at
boot (table migration error), the service struct's store is nil and every
list/get/update/create/reserve method panics on first request.

Apply the same store-nil → errStoreUnavailable pattern from f95a929 to
every remaining method:
* CreateReviewWatch / CreateIssueWatch
* GetReviewWatch / ListReviewWatches / ListAllReviewWatches /
  UpdateReviewWatch / TriggerReviewWatchAll
* GetIssueWatch / ListIssueWatches / ListAllIssueWatches /
  UpdateIssueWatch / TriggerIssueWatchAll
* ReserveReviewMRTask / AssignReviewMRTaskID / ReleaseReviewMRTask
* ReserveIssueWatchTask / AssignIssueWatchTaskID / ReleaseIssueWatchTask
* lookupReviewPolicy / lookupIssuePolicy (return CleanupPolicyNever)

* fix(gitlab): nil-store guards on CheckXxxWatch + DeleteXxxWatch + cleanup paths

Round-4 Claude finding: 4 remaining panic sites the previous nil-guard
pass missed — these run from the background poller (every 5 min) and the
manual delete/cleanup flows, so a boot-time NewStore failure would
crash the orchestrator process on first tick.

* service_watches.go: add store-nil guards to DeleteReviewWatch /
  DeleteIssueWatch / CheckReviewWatch / CheckIssueWatch
* service_cleanup.go: guard CleanupAllReviewTasks / CleanupAllIssueTasks
  entry points; wrap the trailing DeleteReviewMRTask / DeleteIssueWatchTask
  calls in `if store := s.requireStore(); store != nil` blocks so the
  cleanup succeeds even when the dedup-row delete is unreachable

* refactor(gitlab): split service_watches.go to stay under 800-line revive limit

CI's --new-from-rev lint caught service_watches.go at 808 lines (limit
800). Split into three focused files:

* service_events.go (94 LOC) — publish helpers for MR feedback /
  new review MR / new issue / watch lifecycle events
* service_issue_watches.go (302 LOC) — Issue watch CRUD + Check +
  Trigger + fetch + helpers
* service_reservations.go (59 LOC) — Reserve/Assign/Release dedup
  handles used by the orchestrator event handlers

service_watches.go is now 505 lines and contains MR watch + Review
watch only. No behavioral change.

* fix(gitlab): address inline code-review feedback

Address actionable inline comments from Claude/CodeRabbit/cubic/Greptile:

* appendLabelsToQuery: use url.ParseQuery for exact key match instead
  of strings.Contains; previously false-matched keys like mylabels=
  and silently dropped the watch's labels (test added)
* fetchReviewMRs: drop dead `filter = watch.CustomQuery` assignment —
  SearchMRs's buildMRSearchQuery returns customQuery verbatim and
  ignores filter when customQuery is non-empty
* store.go: add workspace_id indexes on gitlab_review_watches and
  gitlab_issue_watches (the 5-min poller and HTTP list endpoints did
  full table scans)
* mock_client.SearchProjects: switch from == to case-insensitive
  Contains so partial-query autocomplete returns the seeded project,
  matching the doc comment
* Poller: add sync.Mutex to guard `started` field; Start/Stop are
  now safe to call concurrently (go test -race would have flagged
  the previous read/write race)
* Add ErrWatchNotFound sentinel; controller_watches.go maps it to
  HTTP 404 via httpRespondError helper. Update/Trigger handlers
  previously returned 500 for missing watches, hiding the distinction
  from real server faults
* service_events.go: log Publish errors on watch lifecycle events
  instead of swallowing them silently
* handlers.go: reject malformed JSON payloads in list-style WS
  handlers rather than silently falling through to broader
  list/search behavior
* event_handlers_gitlab.go: remove dead gitlabRepoSlug helper +
  blank-identifier suppression

* ci: re-trigger to clear flaky E2E shard 3

* fix(gitlab): address final review round — nil guards, store merge, logs, body validation

- CheckMRWatch, UpdateReviewWatch, UpdateIssueWatch: nil guards for store/req
- handlers: wsNewDiscussionNote rejects empty body
- action_presets: Update reads raw stored row (no default freezing);
  Reset validates workspace_id
- service_search.GetStats: log Warn per sub-call failure before zero
- event_handlers_gitlab: log Release* dedup-row errors with context
- web/store.ts: post-slice merge restores every GitLab sub-state field;
  extract buildStateOverrides helper
- web/default-state.ts: add GitLab fields to defaultState +
  mergeInitialState via mergeGitLabFields helper
- web/gitlab-api.ts: UpdateXxxWatchRequest now Omit workspace_id

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-06-02 22:10:07 +01:00
Carlos Florêncio 68aefc0e08 feat(web): system settings pages (status, database, backups, logs, updates, licenses, about) (#942) 2026-05-25 09:04:16 +01:00
João Salavisa 1b58a64fd8 feat(automations): /settings/automations with run-mode + per-automation config (#1016)
* feat: add automation system with triggers, backend-driven metadata, and PR association

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

* feat(automations): execution_mode + flat /settings/automations

Layered on top of #406's automation subsystem:

- Add execution_mode to Automation (task | run, default task). Run-mode
  firings create an ephemeral kanban-hidden task tagged with
  origin=automation_run. The session pipeline still runs against it, but
  users surface them through the AutomationRun history instead of the
  kanban. Pragmatic shim while AutomationRun-as-true-session-owner is
  deferred to a follow-up.

- Thread IsEphemeral + Origin through ReviewTaskRequest +
  reviewTaskCreatorAdapter so the orchestrator can create the hidden
  task in one call.

- Run-mode always auto-starts (the user never sees the task to drag).

- Flat /settings/automations route replaces the per-workspace sidebar
  entry. Picks the only workspace, shows a picker for many, or shows
  a CTA for zero workspaces.

- Editor exposes execution mode + table shows it as a badge column.

Spec at docs/specs/office/automations-settings.md documents the design,
deferred items, and scenarios.

* feat(automations): per-automation repository picker

Adds a repository_id field on Automation so users can pin scheduled and
webhook automations to a specific workspace repository instead of always
falling through to "first repo". github_pr triggers ignore the field and
keep using the PR's own repo — the editor disables the picker with a
hint when a PR trigger is selected.

Backend:
- repository_id column folded into the canonical CREATE TABLE (no
  migration needed — PR #406 introduces the table).
- RepositoryID threaded through model, DTOs, store INSERT/UPDATE, and
  service.CreateAutomation.
- resolveAutomationRepository prefers the explicit ID, falls back to
  workspace-first on empty.

Frontend:
- repository_id on the Automation TS type + Create/Update payloads.
- Repository SelectField in ConfigSection backed by useRepositories;
  "Auto" sentinel represents the empty/fallback choice.
- Disabled with help-text for github_pr triggers.

* feat(automations): repo picker shows discovered repos too

Matches the task-creation dialog's UX: the editor's repository picker
now lists registered workspace repos AND filesystem-discovered repos
under the workspace's roots. Picking a discovered repo registers it
with the workspace at save time (via createRepositoryAction), captures
the new id, and writes it onto the automation. After the first save
the picker selection is promoted from "discovered" to "registered" so
re-saving doesn't re-register.

Form state holds a RepositorySelection union (auto | registered |
discovered) rather than a plain id string so the picker can carry the
metadata needed to register.

github_pr triggers still disable the picker — PR events always override
any configured repo with the PR's own.

* fix(automations): add idempotent ALTERs for in-branch columns

The canonical CREATE TABLE covers fresh installs, but `CREATE TABLE
IF NOT EXISTS` is a no-op for DBs already initialised from an earlier
commit on this branch (PR #406's original schema). Without these
ALTERs, dev DBs created before execution_mode + repository_id existed
on the row hit "no such column" errors.

Three idempotent ALTERs: task_title_template, execution_mode,
repository_id. SQLite returns a duplicate-column error when the column
already exists; we swallow it.

* fix(automations): use generateUUID in non-secure contexts

crypto.randomUUID is only defined in secure contexts (HTTPS or
localhost). PR #406's editor used it directly, which throws "crypto.
randomUUID is not a function" when the app is reached via a plain
hostname like nova:58204. Switch to lib/utils.generateUUID, which
falls back to a Math.random-based implementation when crypto isn't
available.

* fix(automations): polish run-mode lifecycle, validation, and editor UX

Run-mode automations were partly broken end-to-end:
- workflow_id was sent for ephemeral tasks (rejected by task service)
  and was required by the editor + automation service, so "Create
  Automation" was always disabled and silently failed
- a permission_request hanging the agent left the run pinned at
  task_created forever; nothing reaped the ephemeral worktree
- trigger config inputs (cron, branches, authors) did not re-sync
  with the loaded automation on page reload
- runs row never linked back to the underlying task for task-mode
  automations, and the table header highlighted on hover

Backend
- skip workflow fields in CreateReviewTask / StartTask for run-mode
- relax automation.Service.CreateAutomation: workflow only required
  for task-mode
- auto-reject permission prompts on ephemeral automation tasks and
  mark the AutomationRun failed with the requested action
- on agent completion / failure for ephemeral automation tasks,
  flip the run to succeeded/failed and remove the worktree + branch
  immediately (new RunStatusSucceeded + MarkRunSucceededByTaskID)
- wire worktree.Manager into the orchestrator service

Frontend
- show Execution Mode first; hide Workflow / Workflow Step in run-mode
- canSave no longer requires workflow in run-mode
- surface save errors via toast instead of silent failure
- sync trigger-config local input state with loaded prop value
- runs table: clickable row → /tasks/<id> for task-mode runs,
  non-clickable in run-mode; suppress header hover

* fix(automations): address PR review feedback

Backend
- automation/scheduler.go: parseCronFields returns an error for
  unsupported expressions instead of silently defaulting to hourly
- automation/webhook.go: constant-time secret compare, header-only
  (no more ?secret= query param), reject with 409 when no enabled
  webhook trigger exists
- automation/service.go: enforce max_concurrent_runs in FireTrigger
  via maybeSkipForConcurrencyCap (records a skipped run when capped)
- cmd/kandev/e2e_reset.go: surface automation delete errors as 500
  instead of swallowing them
- orchestrator/event_handlers_git.go: auto-reject permission with
  cancelled=true so the message status is "rejected" and the session
  isn't flipped back to running
- orchestrator/event_handlers_agent.go: finalizeAutomationRunIfEphemeral
  now runs at the top of handleAgentFailed, before the resume/recoverable
  early returns, so session-backed run-mode failures get cleaned up

Frontend
- state/slices/automations/automations-slice.ts: declare _get/_api so
  the call site's arity matches and CodeQL stops flagging ignored
  trailing arguments
- automations/schedule-selector.tsx: clearing the input now propagates
  ""; cron is trimmed before validation+save
- automations/triggers-section.tsx: switching to webhook mode deletes
  the existing scheduled/condition triggers so they don't keep firing
  hidden in the background
- automations/trigger-configs/github-ci-config.tsx: useEffect re-sync
  for checkNames, matching the other trigger configs
- hooks/domains/settings/use-automations.ts: per-workspace fetch via
  loadedWorkspaceRef + render-safe loadedWorkspaceId state;
  inFlightWorkspaceRef discards stale responses after workspace switch

---------

Co-authored-by: Carlos Florêncio <carlosmflorencio@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-22 22:01:59 +01:00
Carlos Florêncio 0b2085688b feat(office): autonomous agent management layer (#914) 2026-05-16 20:07:57 +01:00
Carlos Florêncio b1cd2d2076 feat(github): rate-limit awareness, poller throttling, and GraphQL batching (#821) 2026-05-06 16:51:06 +01:00
João Salavisa 83de4793fd feat: add Linear issue watchers (#805) 2026-05-05 08:28:09 +01:00
João Salavisa 5693c10d8c feat(jira): poll JQL queries to auto-create tasks (issue watchers) (#746)
* feat(jira): poll JQL queries to auto-create tasks (issue watchers)

* fix(jira): address review feedback on issue watcher

- Poller: gate per-watch by `LastPolledAt` + `PollIntervalSeconds` so the
  user-configured cadence actually applies (was silently ignored, every
  watch polled every 5 min). Tick interval reduced to 60s to match the
  smallest valid per-watch interval.
- Service: validate `pollIntervalSeconds` against the documented 60-3600s
  bounds on create/update so the UI's range check is enforced server-side.
- Service: stamp `last_polled_at` via a fresh `context.Background()` with a
  short write deadline; the poller's ctx may be cancelled at shutdown,
  which would otherwise drop the timestamp on the floor.
- Service/store: replace per-ticket `HasIssueWatchTask` round-trips with a
  single `ListSeenIssueKeys` set lookup (up to 50× fewer DB queries per
  tick on broad JQL queries).
- Hook: reset cached watch list on workspace switch — the store's `loaded`
  flag is global, so without this the table renders the previous
  workspace's watchers stale.
- Section component: pull the CRUD callbacks from a single
  `useJiraIssueWatches` call and pass them down, instead of invoking the
  hook twice on the same workspace.
- Dialog: add the three placeholders (`{{issue.type}}`, `{{issue.reporter}}`,
  `{{issue.project}}`) supported by the backend interpolator but missing
  from the help text.

* fix(jira): repair workspace-switch refetch + drop double trigger toast

The previous a9bd9fa attempt at the workspace-switch reset was incomplete:
`setJiraIssueWatches([])` flips `loaded=true`, so the fetch effect's
`if (!workspaceId || loaded || loading) return` guard still short-circuited
and the new workspace's data was never fetched. Switching workspaces left
the table empty with no recovery path.

- Add a dedicated `resetJiraIssueWatches` action that zeroes both `items`
  and `loaded`, and use it from the hook's workspace-change effect.
- Drop the inline "Checking JIRA…" toast in the watcher table — the
  parent's `wrappedTrigger` already fires a result toast on completion,
  so the click was double-toasting.

* fix(jira): reject empty workflowId/workflowStepId on issue-watch PATCH

`applyIssueWatchPatch` writes the field whenever the patch pointer is
non-nil, but Go's JSON decoder returns a non-nil pointer to "" for an
explicit `{"workflowId": ""}` body. The post-patch JQL check guarded
against the same hole for `jql`; this extends it to the workflow
fields. Without it, a stripped row would silently drop every event the
orchestrator tries to handle (the watcher looks active but produces no
tasks).

* fix(jira): require workspace ownership on issue-watch mutate/trigger

The PATCH/DELETE/trigger handlers looked up watches by the path :id with
no workspace check, while the list/create handlers correctly required
workspace_id. A user authenticated to workspace A who learned a watch
UUID from workspace B could mutate it, delete it, or trigger it
(running JQL with workspace B's stored credentials and creating tasks
there). Closes the IDOR by:

- backend: PATCH/DELETE/trigger now require ?workspace_id=... and 404
  on mismatch (404 not 403, so probing can't enumerate IDs across
  workspaces).
- frontend: api client + hook thread workspaceId through all mutation
  calls.
- store: drop the no-longer-used HasIssueWatchTask method (replaced by
  ListSeenIssueKeys after the N+1 fix); test now asserts via the
  bulk-fetched set.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-01 11:01:02 +01:00
João Salavisa 3d2a04e776 feat(orchestrator): deferred task move with hand-off prompt + boot/turn-end event split (#743)
* feat(orchestrator): reuse existing same-profile session on workflow step switch

When a workflow step transition needs a different agent profile, look up
an existing session on the same task that already uses the target profile
and promote it back to primary instead of creating a fresh one. Reuses
COMPLETED and FAILED sessions (CANCELLED stays excluded — explicit user
stops shouldn't be auto-revived).

Backend (event_handlers_workflow.go):
  - findReusableSessionForProfile: most recently updated session matching
    the target profile, skipping CANCELLED.
  - reuseSessionForStep: promotes the session to primary; if terminal,
    revives via reviveReusedSession.
  - reviveReusedSession: distinguishes "previously launched" (has
    executors_running record -> flip to WAITING_FOR_INPUT, lazy-resume via
    ResumeSession with stored token) from "never launched" (no record ->
    flip to CREATED so autoStartStepPrompt routes through
    StartCreatedSession for a full fresh launch). Clears stale
    ErrorMessage/CompletedAt so the UI stops surfacing prior failures.
  - createNewSessionForStep + completeAndStopSession: shared helpers.

Backend (task_operations.go):
  - StartCreatedSession no longer calls processOnTurnStartViaEngine.
    User-initiated messages already fire on_turn_start in
    dispatchPromptAsync. Workflow-driven auto-start prompts arrived
    because the workflow just transitioned us into this step via
    on_turn_complete; firing on_turn_start again cascades the workflow
    back out and the redirect-to-active-session logic delivers the prompt
    to the wrong session's agent.

Frontend (web):
  - task.updated handler follows focus to the new primary when the user
    is viewing the task and was sitting on the previous primary.
  - pinnedSessionId tracks user-initiated session selection
    (setActiveSession sets it; setActiveSessionAuto leaves it alone).
    The terminal-handoff branch in maybeAdoptSessionOnTransition skips
    replacement when the terminating session matches the pin, so manually
    clicking a session no longer yanks focus back to the workflow primary
    each time the pinned session terminates.

Tests:
  - TestSwitchSessionForStep_ReusesExistingProfileSession (never-launched).
  - TestSwitchSessionForStep_ReusesPreviouslyLaunchedSession (launched).
  - TestSwitchSessionForStep_ReusesFailedSession (FAILED reuse + cleanup).
  - tasks.test.ts: focus-follow behavior across 4 scenarios.
  - agent-session.test.ts: pinnedSessionId guard plus migration to
    setActiveSessionAuto for WS-driven adoption.

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

* feat(orchestrator): deferred task move with hand-off prompt + boot/turn-end event split

Adds move_task_kandev MCP tool that lets a workflow agent (e.g. QA at "Review")
move a task to another step with a prompt for the receiving agent. The move is
queued as a PendingMove and applied on the calling agent's turn-end so on_enter
processing doesn't race the still-running turn.

Also splits agent.ready into agent.boot_ready + agent.ready at the lifecycle
layer. Previously both signals shared one event and the orchestrator
disambiguated them via a resumeInProgressSessions flag — which had a race
when the boot signal arrived before persistResumeState wrote state=STARTING,
causing the boot to fire on_turn_complete against the just-revived step and
ping-pong the task forward through the workflow.

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

* fix: address PR #743 review feedback (Greptile + CodeRabbit)

Backend:
- isTransientPromptError no longer treats ErrExecutionNotFound as transient,
  preventing executeQueuedMessage from infinite-requeueing on permanent
  missing-execution errors. autoStartStepPrompt's explicit errors.Is check
  before the transient branch keeps the recovery path working (Greptile P2 #1)
- applyPendingMove drains the orphaned hand-off prompt on ApplyTransition
  failure so it can't be misdelivered to the source step on a future turn
  (Greptile P2 #2)
- handleMoveTask returns errors instead of panicking when messageQueue is
  nil; queueMoveTaskPrompt returns an error so the deferred path fails fast
  if the queue can't accept the prompt (CodeRabbit CR-2)
- handleMoveTask rejects when no primary session exists and a hand-off prompt
  is required, instead of silently dropping it (CodeRabbit CR-3)
- autoStartStepPrompt preserves attachments from queued messages (was: silently
  dropped); attachment-only messages are no longer skipped; the fallback path
  also forwards attachments (CodeRabbit CR-5)
- applyPendingMove reinserts the pending move on early failure paths (config
  missing, task/step load errors) so a future turn-end retries naturally
  (CodeRabbit CR-6)
- registerKanbanTools split into 3 helpers (list/profile/task) to satisfy
  backend function-size lint limits (CodeRabbit CR-1)

Web:
- maybeAdoptSessionOnTransition's auto-adopt path also checks pinnedSessionId
  to handle the reverse-event-ordering race (old COMPLETED arrives before new
  STARTING) — pinned sessions are no longer yanked off the user. Regression
  test added (CodeRabbit CR-7)

Tests:
- mockAgentManager records (executionID, prompt) pairs; new helper
  capturedPromptsForExecution actually filters by exec ID instead of
  returning all captured prompts (CodeRabbit CR-4)

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

* fix(web): respect pinned session in primary-follow-focus path

When the user clicks a session, setActiveSession sets pinnedSessionId. If a
workflow profile switch then updates the task's primarySessionId while the
user happens to still be on the previous primary, the focus-follow block in
tasks.ts was calling setActiveSessionAuto(taskId, newPrimary) and silently
overriding the explicit pin. Add pinnedSessionId !== previousPrimary as a
fifth condition. Regression test in tasks.test.ts covers exactly this
sequence.

Addresses Greptile P1 on PR #743.

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

* fix: address CodeRabbit review on PR #743 (3 new comments)

- handleMoveTask idle path now rolls back the queued hand-off prompt when
  taskSvc.MoveTask fails, so a "you were moved..." message can't be delivered
  on a future turn for a transition that didn't actually happen. Added
  TakeQueued to the MessageQueuer interface to support this.
- lookupSession now returns (*models.TaskSession, error) so callers can
  distinguish a backend lookup failure from a legitimate "no primary session"
  state. handleMoveTask maps the error case to ErrorCodeInternalError instead
  of collapsing it into ErrorCodeValidation.
- queueMoveTaskPrompt returns errors instead of log-and-continue (signature
  change already shipped); existing tests updated to reflect the new contract.
- Refactored TestPendingMove_ReviewToInProgress_OneTransitionOnly: the
  ~280-line monolithic test is now split into a pendingMoveScenario builder
  + assertion helpers, each well under the 80-line / 50-statement lint limit.

All 14 review threads resolved.

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

* fix: relax move_task_kandev prompt requirement; fix CI lint

CI surfaced two issues after rebasing onto main:

- nestif lint warning on autoStartStepPrompt's queued-message merge block.
  Extracted the take-and-merge logic into takeAndMergeHandoffMessage helper,
  flattening the nested conditionals.
- E2E test settings/config-management 'agent can move a task to a different
  step' failed because my CR-3 fix made the move_task_kandev prompt parameter
  required. That broke admin/config-mode moves where there's no agent to
  hand off to. Made prompt optional: when supplied AND the source session
  is mid-turn, the deferred-move flow runs (queue prompt + record
  PendingMove); otherwise just move the task. Refactored handleMoveTask
  into deferMoveTaskWithPrompt and applyMoveTaskImmediate to keep each
  function under the 80-line lint limit.

Tool descriptions in mcp/server/server.go and mcp/server/config_handlers.go
updated to reflect prompt's now-optional status. moveTaskHandler in
config_handlers.go forwards prompt only when set.

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

* fix(mcp): treat 'no primary session' as a not-found, not an internal error

GetPrimarySessionByTaskID returns a plain error string ("no primary session
found for task: <id>") when the repo has no rows. My CR-B refactor was
treating ALL lookup errors as ErrorCodeInternalError, which broke the E2E
test 'agent can move a task to a different step' (config-mode move on a
freshly-created task that has no agent yet).

Match the not-found message and return (nil, nil); only real backend
failures bubble up as InternalError. Distinguishing properly with
errors.Is would require an exported sentinel that doesn't exist; matching
the noRowsErr substring is the pragmatic fix.

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-04-30 18:17:36 +01:00
José Almeida 12bd5b2cf3 feat: plan checkpointing with rewind UI (#694)
* feat(backend): plan revision history with checkpoint/revert

Persist every plan edit as an immutable TaskPlanRevision with author
attribution (agent vs user) and expose WS actions to list, fetch, and
revert to past revisions. Emits task_plan.revision.created and
task_plan.reverted events so the UI can react in real time.

* feat(web): plan revision history UI with rewind to past versions

Add a rewind button to the plan panel that opens a revisions list
showing each checkpoint with author (agent name or user) and timestamp,
and lets the user restore any past revision as the new head. Plan
state, hooks, and WS handlers are extended to load, cache, and react
to revision-created/reverted events.

* test(web): e2e coverage for plan checkpointing and rewind

Cover the full revision flow: plan edit creates a checkpoint,
the revisions panel lists authors and timestamps, and reverting
restores the selected revision as the new head. Extends session
page object and backend fixtures with revision-related helpers.

* fix(web): stabilize plan revisions state and avoid re-render loop

Zustand selector returned a fresh [] for missing revisions, fanning out
re-renders that tripped React's infinite-loop guard when the plan panel
mounted. Swap to a frozen empty-array sentinel, auto-load revisions on
mount so WS events fired before connect aren't lost, and gate the
popover's on-open fetch so reopening doesn't re-sort rows mid-click.

* test(web): make plan checkpointing e2e resilient to agent auto-layout

Agent-authored plan writes auto-open the plan panel and can push the
chat tab inactive, so the chat idle placeholder isn't always visible
once the agent finishes. Accept either chat or plan panel as the idle
signal, make openPlanPanel and openRewind idempotent, reopen the
popover after a revert cancel (the confirm dialog closes it), and drop
the toast assertion since no Toaster is mounted in the app.

* fix(backend): atomic plan revision write + review polish

- Add `WritePlanRevision(ctx, head, rev, coalesceLatestID)` repository method
  that wraps HEAD upsert + MAX+1 compute + insert/merge in a single write tx,
  closing the TOCTOU race on `revision_number` and partial-failure divergence
  between HEAD and history. `upsertPlan` and `RevertPlan` both route through it.
- Add `ErrRevisionIDRequired`; return it for empty `TargetRevisionID` (was
  previously conflated with `ErrRevisionNotFound`). Handler maps to validation.
- Backfill fallback `author_kind` changed `"user"` → `"agent"` to match
  `CreateTaskPlan` and the column default.
- Extract shared `revisionSelectCols` constant; rename `max` local (shadowed
  Go builtin) and `dto` local (shadowed package name).

* fix(web): address plan-revisions review feedback

- `mcpWrite` uses `JSON.stringify(content).slice(1, -1)` to escape backslashes,
  quotes, and control chars in one pass — closes the CodeQL finding.
- `clearTaskPlan` now evicts `revisionContentCache` entries for the task's
  revisions before dropping the maps, preventing orphaned cache growth.
- `RevertConfirmDialog` wraps `onRevert` in try/catch/finally so rejections
  surface a toast and the dialog always dismisses.
- Hoist `TaskPlanRevision` import in `task-plan-panel.tsx`.
- `RevisionRow` derives relative time from props every render with a 30s tick,
  so prop changes refresh immediately.
- Strengthen author-switch e2e with `.not.toBeEmpty()` to also reject empty or
  truncated renders.

* fix(web): evict revision content cache on coalesced update

Coalesced writes update an existing revision's content on the backend, but
the WS `upsertPlanRevision` payload carries only metadata. Without eviction,
a cached preview of the revision stays stale after the agent re-writes the
plan within the coalesce window.

* fix(web): plan history UI polish from local testing

- Drop the redundant "Plan" title in the panel header bar; the dockview/mobile
  tab already labels the panel.
- Author display: show "You" with the user icon for user-authored revisions,
  and the agent profile name with the AgentLogo (resolved from the active
  session's `agent_profile_snapshot.agent_name`) for agent revisions, matching
  the icon used elsewhere in the app.
- Replace ScrollArea with a plain `max-h-96 overflow-y-auto` div so the
  popover stays bounded when revision counts grow past ~7 rows.
- Drop the em dash from the revert dialog description.

* feat(web): add lineDiff helper for plan revision compare

Tiny wrapper around the `diff` package that classifies each line as add /
remove / context and strips the trailing-newline phantom entry. Also exposes
`diffSummary` for the dialog header counts. Pure functions; backed by Vitest.

* feat(web): plan revision preview + compare state

Adds two task-scoped store keys for the new dialogs:

- `previewRevisionIdByTaskId` — at most one open preview per task; cleared on
  `clearTaskPlan`.
- `comparePairByTaskId` — ordered 2-slot pair. `toggleComparePair` reuses a
  `nextPair` helper that handles unselect (already-picked id), fill (open
  slot), and FIFO eviction (full pair). `clearTaskPlan` resets it too.

* feat(web): plan revision preview + diff dialogs

- `useTaskPlan` exposes `previewRevisionId`, `setPreviewRevision`,
  `comparePair`, `toggleCompareSelection`, `clearComparePair` (extracted into
  `usePreviewCompareState` to keep the surrounding hook within the size cap).
- New `PlanRevisionPreviewDialog`: read-only modal for a single revision with
  Restore + "Compare with current" CTAs. Restore routes through the existing
  RevertConfirmDialog so the confirmation copy stays consistent.
- New `PlanRevisionDiffDialog`: line-level unified diff between two revisions
  with adds/removes highlighted; orders the pair by revision_number so the
  older one is always "before"; Restore targets the older side.
- Popover row split into a clickable body (opens preview), a Compare toggle
  (icon button per row), and the existing Revert button. Compare chips appear
  at the top of the popover with a Diff CTA when both slots are filled.
- Both dialogs are remounted via key on revision/pair change so loader state
  resets without ESLint-flagged setState-in-effect resets.

* test(web): e2e coverage for plan preview and compare-diff dialogs

Extends the SessionPage POM with locators for the new preview and diff
dialogs (chips, compare toggle, restore buttons, body + summary). Adds two
scenarios in plan-checkpointing.spec.ts:

- Click a revision row body → preview shows the right content; the Restore
  CTA opens the existing confirm dialog and the revert lands.
- Toggle compare on two revisions → diff opens with v1 → v2 ordering and
  shows at least one add and one remove line; the Restore CTA targets the
  older side.

* feat(web): render markdown in plan preview + diff dialogs

Adds a shared `PlanReadOnlyMarkdown` Tiptap renderer that reuses the same
StarterKit + tiptap-markdown + tables/tasks/code-block extensions as the live
plan editor (minus interactive bits — slash menu, drag handles, comment
marks). Wired into both dialogs:

- Preview replaces the raw `<pre>` block with the rendered Tiptap view.
- Diff dialog now stacks two rendered sides (Before / After) with red/green
  tinted borders, kept above the existing line-level breakdown so the user
  gets both rendered comparison and an exact textual change list.

Tiptap doesn't ship a free diff/track-changes extension (it's part of Tiptap
Pro), so the side-by-side rendering plus the line-level list together stand
in for that.

* fix(web): plan diff dialog — text-only with unified/split modes

Following local feedback:

- Drop the rendered-markdown side-by-side panes from the diff dialog. Diff
  view is text-only again, like a code review.
- Add a Unified / Split mode toggle. Split mode aligns each remove with the
  adjacent add on the same row (single-line edits stay paired) and stripes
  empty cells where one side has nothing.
- Fix the highlight-truncation bug: long lines used to lose their tinted
  background past the viewport on horizontal scroll because the row width
  was capped to the visible area. Rows now use `min-w-full w-max` so the
  background extends through the full scroll width.

Preview dialog keeps the rendered Tiptap markdown — only the diff reverted
to plain text.

E2E covers both modes and the cell kinds.

* feat(web): formatPreciseTime util for minute-level timestamps

Returns 'just now', 'Xs ago', 'Xm ago' for the first hour, then 'Today, HH:MM',
'Yesterday, HH:MM', or '{Mon} {DD}[, YYYY], HH:MM' for older entries — keeps
exact minute-of-day visible instead of decaying to 'yesterday'/'2d ago'.

* fix(web): render markdown via setContent in plan read-only renderer

The tiptap-markdown extension's parser only fires on `setContent` calls, not
on the initial `content` option. Passing `content: value` to `useEditor` was
treating the markdown source as raw text, so the preview dialog rendered
unparsed markdown.

Drop the `content` option and instead call
`editor.commands.setContent(content, { emitUpdate: false })` from a useEffect
once the editor is created — which is what the live plan editor effectively
relies on through tiptap-markdown's setContent override.

* fix(web): plan history UX — bigger modals, split layout, simpler compare

- Bump dialog widths: preview now `max-w-5xl w-[92vw]`, diff
  `max-w-6xl w-[95vw]`, both with taller max-height. Plenty of room for
  rendered markdown and side-by-side diffs.
- Replace the table-based split layout with a CSS grid (`grid-cols-2 w-max`).
  The table-fixed cells were overflowing into each other when content was
  wider than half the dialog; the grid lets the natural width drive a single
  horizontal scrollbar on the parent and keeps the two columns properly
  separated.
- Switch row timestamps to `formatPreciseTime` so users see minute-of-day
  ("Today, 14:32", "Yesterday, 09:14", "Apr 21, 11:08") instead of decaying
  to "yesterday".
- Simplify the compare flow: drop the per-row Compare toggle, the chip strip
  at the top of the popover, and the standalone "Diff" button. Compare is
  now a side effect of preview — open any version, then click "Compare with
  previous" or "Compare with current". E2E test updated to use the new path.

* test(web): update plan checkpointing e2e for the new compare flow

Drops the locators for the row Compare toggle, chips, and standalone Diff
button (those UI bits are gone). Adds a locator for the new
"Compare with previous" preview button. The compare scenario now opens the
diff via preview → "Compare with current" instead of the old chip flow.

* fix(web): preview renderer reuses live editor's markdown styles

`globals.css` keeps heading sizes / list spacing / code-block padding under
.markdown-body and .tiptap-plan-wrapper, and the live editor opts in by
applying both classes (plus a conditional .dark for theming). The read-only
renderer was using `prose prose-sm` instead, which doesn't actually take
effect on the .ProseMirror node — leaving everything the same size.

Switch the read-only renderer to the same class set, follow the live
editor's `useTheme` -> `dark` pattern, override `!h-auto` so the renderer
sizes to its content, and trim the editor's default 0.75rem padding inside
the dialog body.

* fix(web): split diff has fixed width with synced horizontal scroll

The previous split layout grew the whole dialog wider when content overflowed
which felt wrong. Now:

- The dialog stays at its fixed width.
- Each pane has its own `overflow-x-auto` so each side is capped at half the
  dialog and gets its own horizontal scrollbar.
- A scroll listener keeps both panes' `scrollLeft` in lockstep (with a
  re-entrancy guard so the bounce-back doesn't form a loop) — corresponding
  rows stay aligned visually no matter which side the user scrolls from.
- The diff body's overflow-x is hidden in split mode (the panes own it) and
  only overflow-y stays on the body so vertical scroll covers both panes
  at once.

* feat(backend): resolve agent profile name from session for plan revisions

The MCP-side handlers (handleCreateTaskPlan / handleUpdateTaskPlan) don't
carry the agent's display name in the request, so without a lookup every
agent-authored revision was rendering as the literal "Agent" fallback in
the history UI.

PlanService now looks up the task's session and reads the agent profile
display name from `agent_profile_snapshot` (preferring "label", then
"name", then "agent_display_name") when the request is an agent write
without an explicit author_name. Active session is tried first; falls
back to the most recent session by started_at so plans written between
turns still get the right author name. If no usable session/snapshot
exists, the existing "Agent" fallback applies.

The repo dependency type is widened from `repository.PlanRepository` to a
small private `planRepo` interface that adds the two session lookups —
the SQLite repository (`*sqliterepo.Repository`) already implements both,
so no caller wiring changes.

* fix(web): plan history popover row + restore button polish

- Popover content's default `gap-4` between flex children (from
  @kandev/ui's PopoverContent) was rendering as visible space above the
  top-most row. Override with `gap-0` so the list sits flush against the
  header.
- Switch row layout from `items-start` to `items-center` so the
  Restore button stays vertically centered against the row body even
  when the body grows taller (timestamp + restored-from marker on revert
  rows).
- Rename the per-row "Revert" button to "Restore" and prefix it with
  IconRestore so it matches the dialog wording and the restored-from
  marker icon. The data-testid stays `plan-revision-revert-button` for
  e2e selector stability.

* fix(backend): plan revision review feedback + lint

- `wsGetTaskPlanRevision` now returns a 400 validation error when
  `revision_id` is empty, matching `wsRevertTaskPlan`'s
  `ErrRevisionIDRequired` branch (was silently mapped to 404 via the
  service's lookup miss).
- `agentDisplayNameFromSnapshot` tries `name` first, then `label`, then
  `agent_display_name`. The orchestrator's canonical key wins over any
  stale older `label` that may still be in a session snapshot.
- Add `idx_task_plan_revisions_task_number` on
  `(task_id, revision_number DESC)`. `GetLatestTaskPlanRevision` runs on
  every plan write (coalesce check); the existing `(task_id, created_at)`
  index doesn't help since these queries order by `revision_number`.
- Drop `Deprecated:` from `NextTaskPlanRevisionNumber`'s doc comment so
  staticcheck stops flagging the existing test usage. The TOCTOU caveat
  stays in the prose.

* fix(web): plan revision review feedback, prettier, e2e resilience

- `loadRevisionContent` reads `revisionContentCache` lazily through the
  store API inside the callback, so its identity stays stable. Selecting
  the cache map at the hook level was recreating the callback whenever
  any task's content was cached, retriggering downstream content-fetch
  effects.
- `task.plan.reverted` WS event becomes a no-op handler.
  `task.plan.revision.created` fires for the same row first and already
  upserts; the duplicate handler was double-evicting the
  `revisionContentCache` entry and triggering an extra Zustand update.
- e2e: agent author assertion now uses `not.toHaveText("You")` +
  `not.toBeEmpty()` instead of the literal `"Agent"` (which only
  matched the legacy fallback — agent rows now resolve to the profile
  display name).
- e2e: `openPlanPanel` retries the toggle a few times after a page
  reload — the panel toggle can race hydration and silently no-op,
  which broke the persistence test.
- Apply prettier to the files flagged by the formatting check.

* fix(plan): toast on silent revert failure + revision ownership check

- task-plan-revisions: when `onRevert` (the hook's `revertTo`) swallows
  the error and returns null, the dialog used to close silently with no
  feedback. Add an `else` branch that fires `toast.error("Failed to
  restore plan")`.
- wsGetTaskPlanRevision: accept an optional `task_id` and reject with a
  400 "Revision does not belong to task" when the loaded revision's
  task doesn't match. Mirrors `wsRevertTaskPlan`'s ownership check so a
  WS client can't read content for an arbitrary revision UUID outside
  the tasks it already holds a reference to.
- plan-api `getPlanRevision` takes optional `taskId`; `loadRevisionContent`
  threads the active task id so the check is exercised on the happy path.
2026-04-27 10:22:58 +01:00
João Salavisa c11a6bbfd2 feat(github): add issue watcher with task creation and auto-cleanup (#672)
* feat(github): add issue watcher with task creation and auto-cleanup

Add a GitHub Issue Watcher feature that polls for new issues matching
configurable filters and automatically creates tasks for them. This
complements the existing PR Review Watcher with the same patterns.

Backend:
- Issue watch CRUD (models, store, service, controller, WS handlers)
- Poller integration with configurable intervals
- Event-driven task creation via orchestrator
- Repository resolution and worktree support for issue tasks
- Auto-cleanup of tasks for closed issues (CleanupClosedIssueTasks)
- GetIssueState client method across all implementations
- Default branch auto-detection from cloned repo HEAD

Frontend:
- Issue watch settings UI (table, dialog, placeholders)
- Issue link icon on task cards (green circle-dot)
- API client, store slice, and hook for issue watches
- SSR and WS hydration of issue metadata

* fix: resolve golangci-lint errors (goconst, nestif, dupl) and prettier formatting

* fix(github): fix issue watch update shape, trigger key, and label quoting

- httpUpdateIssueWatch now returns the full IssueWatch object instead of
  {updated:true} so the frontend store correctly refreshes after edits
- httpTriggerIssueWatch returns new_issues_found to match TriggerIssueResponse
  type; was returning new_issues so the toast count was always 0
- buildIssueFilter uses conditional quoting instead of fmt.Sprintf %q to
  avoid Go escape sequences in GitHub search qualifiers

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* fix: resolve eslint warnings (max-lines-per-function, no-nested-ternary)

* test(github): add issue watcher tests and fix review feedback

- Add TestInterpolateIssuePrompt (4 cases: empty/all/none/partial)
- Add TestCreateIssueTask (3 cases: skip/reserve+assign/release on fail)
- Add TestShouldDeleteIssueTask (4 cases: open/closed+sessions/closed/error)
- Fix WS response key new_issues -> new_issues_found
- Fix fmt.Printf -> fmt.Fprintf(os.Stderr) in store.go
- Add CleanupClosedIssueTasks call in WS trigger handler

* fix: address PR review feedback and CI failures

- Split test files to fix revive file-length-limit (service_test.go,
  event_handlers_github_test.go)
- Add session checker guard in CleanupClosedIssueTasks
- Normalize poll intervals on UpdateIssueWatch
- Add cleanup call in TriggerAllIssueChecks loop
- Log cleanup errors in controller and WS handler
- Prefer refs/remotes/origin/HEAD in detectGitDefaultBranch
- Add aria-label to issue task icon
- Fix TypeScript type errors: add linkedIssues to GitHubSlice,
  fix hook exports, remove invalid imports

* fix: split test files exceeding revive limit and clean up minor issues

- Split service_test.go (883→779 lines): move TestTriggerPRSync_* to
  service_pr_sync_test.go
- Split event_handlers_github_test.go (806→677 lines): move
  TestCreateReviewTask_* and helpers to event_handlers_github_review_test.go
- Remove redundant s.taskSessionChecker nil-check inside shouldDeleteIssueTask
  (early guard at CleanupClosedIssueTasks already ensures it is non-nil)
- Remove unreachable dead guard in detectAndPersistDefaultBranch
  (repo.DefaultBranch is always "" at that call site)
- Fix bogus #0 issue badge in task-session-sidebar.tsx when issueNumber is absent

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* fix: address Claude review — poll clamp and prompt injection

- Handle v <= 0 in UpdateIssueWatch poll interval (fall back to default)
- Remove {{issue.body}} from default template to prevent prompt injection
- Update test expected output for new default template

* fix: remove {{issue.body}} from frontend DEFAULT_ISSUE_WATCH_PROMPT

Mirrors the backend fix — the UI pre-fills the prompt so the backend
fallback was never reached. The placeholder stays in
ISSUE_WATCH_PLACEHOLDERS for explicit opt-in.

* feat(github): add isIssueWatch sidebar filter dimension

Add isIssueWatch boolean to KanbanState, TaskSwitcherItem, and sidebar
filter registry — mirroring the isPRReview pattern. Issue-watch tasks
(created from issue_watch_id in metadata) can now be explicitly filtered
in the sidebar, just like PR review tasks.

Files changed:
- sidebar-view-types: add isIssueWatch dimension
- filter-dimension-registry: add Issue watch filter meta
- apply-view: add isIssueWatch extractor
- kanban types: add isIssueWatch field
- TaskSwitcherItem: add isIssueWatch field
- use-all-workflow-snapshots: derive isIssueWatch from metadata
- ssr/mapper: derive isIssueWatch from metadata
- task-session-sidebar: pass isIssueWatch, extract helpers to fix complexity

* fix: correct deleteIssueWatch response type to match backend

Backend httpDeleteIssueWatch returns {"deleted": true}; the frontend
type was { success: boolean } — a mismatch with no functional impact
(return value is ignored) but wrong per TypeScript contract.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* refactor: extract metadata helpers to shared lib/metadata-utils.ts

- Move isPRReviewFromMetadata, isIssueWatchFromMetadata, and
  issueFieldsFromMetadata to a shared module to eliminate duplicate
  definitions (sonarjs/no-identical-functions).
- Fix deleteIssueWatch response type: { success: boolean } -> { deleted: boolean }
  to match the actual backend response.

* style: fix prettier formatting

* test(web): add tests for isIssueWatch filter dimension and metadata-utils

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: fix prettier formatting in test files

* fix(github): cascade-delete issue watch tasks on watch deletion and add isPRReview to SSR mapper

- DeleteIssueWatch now wraps in a transaction and deletes related
  github_issue_watch_tasks rows before removing the watch; prevents
  orphaned dedup rows accumulating after a watch is deleted
- snapshotToState in mapper.ts now also sets isPRReview from metadata,
  matching the mapping already done in use-all-workflow-snapshots.ts

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: extract duplicate string constant in metadata-utils test

* style: extract repeated WS error strings into constants in handlers.go

goconst min-occurrences: 3 — 'invalid payload' (6x) and 'id required' (4x)
both exceed the threshold. Extract to package-level constants.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: extract repeated issue key strings into constants in event_handlers_github.go

goconst min-occurrences: 3 — 'issue_number' (9x) and 'issue_watch_id' (5x)
both newly introduced in this PR exceed the threshold.

Co-authored-by: João Salavisa <undefined@users.noreply.github.com>

* style: fix prettier formatting in metadata-utils test

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:32:46 +01:00
José Almeida f1da9644f4 feat: agent-native config mode (#396)
* feat: add MCP mode pipeline for config vs task tool registration

Thread McpMode field through the entire instance creation chain:
CreateInstanceRequest → InstanceOverrides → InstanceConfig → MCP Server.
This allows config-mode MCP sessions to register a different set of
tools than the default task-mode sessions.

* feat: add config-mode MCP tools and agentctl handlers

Register config-mode tools (workflow steps, agents, MCP config, tasks)
when MCP server mode is "config". Add 16 new WS action constants and
agentctl-side handlers that forward tool calls to the backend via the
existing BackendClient channel.

* feat: add backend MCP handlers for config-mode operations

Add backend WebSocket handlers for workflow step CRUD, agent CRUD,
MCP config get/update, and task move/delete/archive/state operations.
Wire config dependencies via SetConfigDeps pattern to avoid breaking
the existing NewHandlers constructor.

* feat: add config-mode system prompt and unit tests

Add ConfigContext system prompt listing all config-mode MCP tools.
Add unit tests for mode-based tool registration, agentctl config
handler forwarding, backend handler validation, and system prompt
content.

* test: add exhaustive E2E tests for config-mode MCP handlers

38 end-to-end tests covering the full handler → service → DB pipeline
for all 16 config-mode operations: workflow step CRUD, agent CRUD,
agent profiles, MCP config, and task management (move, delete, archive,
update state). Includes handler registration verification and two
full pipeline integration tests.

* fix: remove E2E test that depends on agent CLIs installed in CI

TestE2E_Agent_CreateMultipleAndList tried to create claude-code and
codex agents, which aren't available in CI. Agents are unique by type
ID so creating duplicates also fails. Removed the test since
CreateAndList already validates the create+list flow.

* test: remove Go E2E tests for config-mode handlers

Replacing with Playwright E2E tests.

* test: add Playwright E2E tests for config-mode features

13 tests covering workflow steps, task operations, agent/profile
settings pages, MCP config, and workflow settings round-trips.

* feat: add dynamic MCP mode reconfiguration for agentctl instances

Add SetMode() to the MCP server that clears and re-registers tools for
the new mode. Expose it via PUT /api/v1/mcp/mode on per-instance servers,
a SetMcpMode client method, and a lifecycle Manager.SetMcpMode wrapper.

This enables switching from task to config MCP tools on workspaces that
were prepared before plan_mode was set on the session.

* feat: wire McpMode through executor launch and resume pipelines

Add McpMode to LaunchOptions so callers can set it at launch time.
StartTask and StartTaskWithSession now pass McpMode="config" when
plan mode is active. startAgentOnExistingWorkspace reconfigures the
MCP server via SetMcpMode before starting the agent subprocess.

The AgentManagerClient interface gains SetMcpMode; the lifecycle
adapter, executor, and all mock implementations are updated.

* fix: pass plan_mode from workflow step when auto-starting moved tasks

handleTaskMovedNoSession was hardcoding planMode=false when calling
StartTask. Now it loads the target step and checks for enable_plan_mode
in on_enter actions, passing the correct value so config-mode MCP tools
are registered for the agent.

* test: fix config-mode E2E tests and add SetMcpMode mock stubs

- Add SetMcpMode stubs to all AgentManager mock implementations
- Fix strict mode violations with { exact: true } on tool name assertions
- Expand collapsed tool call groups before asserting individual names
- Remove temporary diagnostic code

* fix: address CodeRabbit review comments

- Add servers parameter to update_mcp_config tool schema
- Add mutex protection to SetMode for thread safety
- Create session before building prompt so config context has session ID
- Fix inconsistent ask_user_question tool name reference (use _kandev suffix)

* refactor: use canonical ws.ActionMCP constants and handle marshal errors

- Import ActionMCP* constants from pkg/websocket instead of duplicating
  them locally in config_handlers.go and handlers.go
- Handle json.MarshalIndent error in forwardToBackend instead of ignoring it

* fix: extract McpModeConfig constant and fix Prettier formatting

- Add McpModeConfig constant to avoid repeated "config" string (goconst)
- Fix Prettier formatting in config-management E2E spec

* fix: validate task state before update and strengthen sysprompt tests

- Add TaskState validation in handleUpdateTaskState to return proper
  validation error instead of internal error for invalid states
- Strengthen format-contract assertions (exact %s counts, no %! markers)
- Extract repeated test data into constants per project guidelines

* refactor: decouple config MCP mode from plan_mode, use session metadata

Replace plan_mode-based config MCP activation with config_mode in
session metadata. Add isConfigModeSession() helper used across
executor_execute, executor_interaction, and executor_resume.
Propagate task.Metadata to session.Metadata in PrepareSession so
config_mode flows from task creation through to the agent launch.

* refactor: remove task tools from config MCP mode and update sysprompt

Remove task management tools (list_tasks, move_task, delete_task,
archive_task) from ModeConfig registration — they remain available in
ModeTask. Update ConfigContext system prompt to remove task tool docs
and add example requests section. Update MCP server tests for new tool
count (18→14) and remove deleted task handler tests.

* feat: add config-chat endpoint and workspace config agent profile

Add default_config_agent_profile_id column to workspaces with SQLite
migration. Add POST /workspaces/:id/config-chat endpoint that creates
an ephemeral task with config_mode metadata and starts a session with
config MCP tools. Extract resolveConfigChatDefaults helper to stay
within lint complexity limits.

* feat: add config chat UI with workspace settings integration

Add config chat modal component and hook for starting config sessions.
Add configChat state slice with open/close actions. Add workspace API
client for config-chat endpoint. Add configuration agent profile
selector to workspace settings form. Add config chat button to
settings layout sidebar.

* test: rewrite config-mode E2E tests to use metadata and session_id

Rewrite config-management E2E tests to use createTaskWithAgent with
config_mode metadata instead of a custom fetch helper. Fix navigation
to use task.session_id instead of task.id. Add metadata parameter
support to ApiClient.createTaskWithAgent.

* fix: use floating FAB for config chat and pass default agent profile

Move config chat button from header to a floating action button in the
bottom-right corner with a sparkles icon. Pass the workspace's default
agent profile ID when opening the config chat to avoid "no agent
profile configured" error. Add default_config_agent_profile_id to
workspace slice type.

* feat: async agent launch for config-chat and extract helpers

The config-chat endpoint now launches the agent asynchronously when a
prompt is provided, matching the start_agent flow used by task creation.
Extract deleteTaskOnError and launchConfigChatAgent helpers. Remove
LaunchWorkspace from prepare step to avoid blocking.

* feat: first-interaction agent profile selection for config chat

Open the config chat modal immediately on FAB click. If no default
config agent profile is set, the empty state now shows a profile
selector before starting. The selected profile is saved as the
workspace default_config_agent_profile_id for future sessions.

* test: fix config E2E tests — remove duplicate method, use real tools

Remove duplicate startConfigChat method in ApiClient that overrode the
correct 3-param signature. Replace invalid create_agent test (requires
registered agent type) with update_agent test. Remove fragile tool calls
group UI assertion from multi-tool test.

* fix: resolve 'no active session' error in config chat

Add openConfigChatModal action that only sets isOpen and workspaceId
without touching sessionId. Previously, open() called openConfigChat
with empty sessionId which was truthy, causing QuickChatContent to
render with an invalid session.

* feat: add minimal toolbar mode for config chat input

Thread minimalToolbar prop through ChatInputArea → ChatInputContainer →
ChatInputBody → ChatInputToolbar → QuickChatContent. When enabled,
renders only the submit/cancel button, hiding plan toggle, MCP
indicator, context popover, model selector, and other controls.

* feat: replace config chat dialog with floating popover panel

Replace Dialog with Popover from @kandev/ui so the config chat floats
up from the FAB button. Reduce FAB size to h-12/w-12 with larger h-7/w-7
icon. Rename tooltip to "Configuration Chat". Remove standalone
ConfigChatFAB component from settings layout.

* feat: filter config chat tasks from quick chat listing

Add exclude_config query parameter to ListTasksByWorkspace that filters
out tasks with metadata.config_mode=true. Apply it in the quick chat
sessions API call to prevent config chat tasks from appearing as tabs.

* refactor: move config agent profile setting to agents page

Remove the Configuration Agent Profile selector from workspace settings
and add a dedicated ConfigChatAgentSection to the agents settings page
with auto-save on change.

* feat: redesign config chat empty state with inline text input

Replace clickable template buttons with subtle suggestion hints that
fill the textarea on click and disappear once the user starts typing.
Add a persistent Textarea + send button at the bottom so users can
type directly without clicking templates. This prevents the "no active
session" error since QuickChatContent only renders after startSession
succeeds.

* fix: config chat UX polish — non-clickable suggestions, profile persistence

- Make suggestion prompts purely visual (non-interactive text hints)
- Update workspace store after saving config agent profile so the
  profile selector doesn't reappear on subsequent opens
- Use useAppStoreApi for store access in callbacks

* fix: add inline error display and prevent accidental popover dismiss

- Show API errors inline below the input instead of via toast (which
  can be hidden behind the popover)
- Prevent popover from closing on outside click via onInteractOutside,
  so the user doesn't lose their typed input or active session
- Surface error state from useConfigChat hook to the empty state UI

* fix: use config-specific placeholder in config chat input

Add placeholderOverride prop through ChatInputArea and QuickChatContent
so the config chat shows "Ask anything about your configuration..."
instead of the default "Continue working on the task..." placeholder.

* feat: add default_config_agent_profile_id to workspace DTO and API

Wire the field through WorkspaceDTO, FromWorkspace(), HTTP/WS request
structs, service layer, and public API types so the config chat default
profile persists across page reloads and SSR hydration.

* feat: add config chat store actions and prompt delivery via WS

Add configChat slice actions (open, close, rename, tab management),
pendingPrompt state in useConfigChat hook, and initialPrompt support
in QuickChatContent so prompts are delivered via WS after subscription
is established rather than via HTTP (avoiding race condition).

* fix: resolve dual store issue and message fetch race condition

Replace StateProvider with StateHydrator in settings layout to prevent
a shadow Zustand store that intercepted WS events. Fix message fetch
race condition by merging WS-delivered messages with fetch results
instead of replacing them.

* feat: make config chat global with command panel integration

Add ConfigChatProvider at root layout so the popover is available on all
pages. FAB button only visible on settings pages; hidden anchor on other
pages for popover positioning. Add "Configuration Chat" command to the
command panel (Cmd+K). Add close button and header bar to the popover.

* test: add E2E tests for config chat popover and command panel

Add tests for opening config chat via FAB on settings page (with full
agent response flow) and opening via command panel on kanban page.
Fix api-client endpoint and add workspace update fields for E2E setup.

* fix: compress props destructuring to satisfy max-lines-per-function lint

Rebase merged both sides' additions into ChatInputEditorArea and
ChatInputToolbar, pushing them over the 100-line function limit.
Use object parameter with compact destructuring to reduce line count.

* fix: register config task tools and add exhaustive E2E coverage

Config-mode MCP task tools (list_tasks, move_task, delete_task,
archive_task) were defined but never registered — registerConfigTaskTools()
was never called in registerTools(). Fixed the registration and expanded
E2E tests from 5 to 15, covering all testable config-mode MCP tools
(workflow, agent, MCP config, and task management) with API-side
verification of side effects.

* feat: replace create/delete agent with create/delete agent profile in config mode

Agent creation/deletion via MCP isn't practical since agents map to
registered types. Replace with create_agent_profile and
delete_agent_profile tools that manage the per-agent configuration
presets users actually need to work with. Wires full chain: agentctl
MCP tools → WS actions → backend handlers → controller layer.

* chore: fix Prettier formatting

* fix: compress props destructuring to satisfy max-lines-per-function lint

* test: update MCP tests for agent profile tools and correct config tool count

* fix: address CodeRabbit review — normalize MCP mode, set default, consolidate tests

* fix: address CodeRabbit review — error leaks, search filter bug, stale prompt, sysprompt tools

* feat: expose all workflow step fields in config-mode MCP tools

* feat: add workflow CRUD to config-mode MCP tools

* fix: split workflow CRUD E2E test into separate tests

* fix: prevent cascade deletion of config session in delete workflow E2E test

The config-chat endpoint picks the newest workflow (ORDER BY created_at
DESC) for the ephemeral task. When the test created a deletable workflow
right before starting the config session, the session's own task landed
under that workflow. Deleting the workflow then cascaded and wiped the
running session's task, causing the MCP tool call to hang.

Add a "Config Anchor" bumper workflow so the config session uses it
instead of the deletable one.

* feat: add executor management to config-mode MCP tools

Add 8 new MCP tools for executor and executor profile CRUD in config
mode: list/create/update/delete for both executors and profiles.
Wired through the full stack (WS actions, backend handlers, agentctl
tools, sysprompt) with unit tests and E2E coverage.

* fix: auto-focus config chat input and rename popover title

* fix: remove executor CRUD MCP tools, keep executor profile management

Executors are pre-defined system entities (local, worktree, docker,
sprites). Only executor profiles should be managed via config chat.
Removes create_executor, update_executor, delete_executor MCP tools
while keeping list_executors and all executor profile tools.

* fix: add realtime UI sync for workflow steps and agent profiles via MCP

Workflow step and agent profile changes made through the config chat
MCP tools were not broadcasting events to the frontend. Executor
profile events worked because they went through the task service which
has event bus integration.

- Add WorkflowStep and AgentProfile event types to the event bus
- Add WS notification action constants for workflow step events
- Publish events from MCP handlers after step/profile CRUD operations
- Register gateway subscriptions for the new event types
- Add frontend WS handlers for workflow.step.created/updated/deleted

* fix: reuse existing config chat session when opened from command panel

The command panel always called openConfigChatModal() which reset the
session state, causing the profile picker to show again. Now it checks
for an existing active session first, matching the FAB behavior.

* fix: suppress FAB tooltip while config chat popover is open

When closing the popover via the X button, the mouse ends up over the
FAB area and the tooltip re-appears. Force the tooltip closed while
the popover is open so it cannot re-trigger on close.

* fix: thread default_config_agent_profile_id through all workspace data paths

The field was defined on the workspace type but never populated in:
- SSR mapper (page.tsx) — missing from initial hydration
- WS workspace handlers — wiped on workspace.created/updated events
- Backend publishWorkspaceEvent — not included in event data

This caused the config chat to always show the profile picker when
opened from the command panel, since the saved default was lost.

* fix: address PR review comments — workspace sync, type fixes, missing tool doc

- Sync configChat.workspaceId when promoting next tab in closeConfigChatSession
- Add update_task_state_kandev to ConfigContext system prompt
- Fix workflow step WS event types (workflow.step.* instead of step.*)
- Fix const reassignment in E2E tests (const → let)

* refactor: clean up dead code and register missing MCP tools

- Register update_task_state, delete_workflow_step, and
  reorder_workflow_steps as MCP tools in agentctl config mode
- Remove dead handleCreateAgent/handleDeleteAgent WS handlers
  and their ActionMCP constants (only profiles are managed)
- Rename openConfigChatModal to startNewConfigChat across frontend
- Update sysprompt, tests, and tool counts

* fix: remove extra next.config.ts machine

* fix: address CI failures — prettier formatting and funlen split

* fix: address coderabbit review comments across config-mode stack

- Include prompt field in workflow step broadcast payloads
- Align sysprompt task-state values with actual backend enum
- Return error instead of continuing when MCP mode switch fails
- Validate mode values in agentctl set-mcp-mode endpoint
- Use errors.Is() for sentinel error comparison
- Fix nullish coalescing to allow clearing config agent profile
- Expand StepPayload type with missing config-step fields
- Derive config agent Select value from store instead of local state

* fix: reuse persisted config chat session and reduce toolbar complexity

- Look up configChat.sessions for workspace before creating new session
- Extract MinimalToolbar and move defaults to module-level object to
  reduce ChatInputToolbar cyclomatic complexity from 16 to 7

* fix: E2E executor test strict mode violation and invalid cleanup

- Use exact match for list_executors text to avoid matching 2 elements
- Remove cleanup that tried to delete pre-existing system executor
- Reuse persisted config chat session for workspace in global commands
- Reduce ChatInputToolbar cyclomatic complexity with module-level defaults
2026-03-13 00:44:06 +00:00
Carlos Florêncio bb1caaba11 feat: add ACP agent variants for Claude, Codex, Copilot, and Amp (#387) 2026-03-10 17:30:04 +00:00
Carlos Florêncio 4336f2e4f8 feat: improve mcp ask user debug (#376) 2026-03-05 20:28:13 +00:00
Carlos Florêncio c73fa7471b fix: resolve clarification MCP timeout with cancel-and-resume flow (#362) 2026-03-05 00:18:57 +00:00
Carlos Florêncio d8264823d3 feat: improve workflow auto start (#291)
release / build-web (push) Has been cancelled
release / build-bundles (aarch64-linux-gnu-gcc, arm64, linux, ubuntu-latest, linux-arm64) (push) Has been cancelled
release / build-bundles (amd64, darwin, macos-14, macos-x64) (push) Has been cancelled
release / build-bundles (amd64, linux, ubuntu-latest, linux-x64) (push) Has been cancelled
release / build-bundles (arm64, darwin, macos-14, macos-arm64) (push) Has been cancelled
release / build-bundles (x86_64-w64-mingw32-gcc, amd64, windows, ubuntu-latest, windows-x64) (push) Has been cancelled
release / publish-release (push) Has been cancelled
2026-02-26 11:44:41 +00:00
Carlos Florêncio aea8dc7b67 feat: improve workflows and agent resume (#269) 2026-02-23 23:54:27 +00:00
Carlos Florêncio dff0b49c51 docs: add github integration (pr watcher) (#262) 2026-02-23 00:16:37 +00:00
Carlos Florêncio a23ab5be3a feat: remote executors + secrets (#257) 2026-02-21 22:14:56 +00:00
Carlos Florêncio 01b0e9edf1 feat: improve acp + tracing (#258) 2026-02-20 17:30:14 +00:00
Carlos Florêncio 61ef224ec7 style: fix format issues (#255) 2026-02-19 22:18:01 +00:00
Carlos Florêncio 2b05ed0735 feat: improved passthrough and workflow (#237) 2026-02-17 12:32:02 +00:00
Carlos Florêncio 658bd349b5 feat: improve workflows (#228) 2026-02-16 13:48:58 +00:00
Carlos Florêncio cc17ae7813 feat: queue messages (#201) 2026-02-07 16:04:01 +00:00
Carlos Florêncio 5b6be01156 feat: slash commands from agents (#136) 2026-02-01 01:20:13 +00:00
João Salavisa 6eb24006f9 feat: implement task plans feature (#131) 2026-01-31 00:51:17 +00:00
João Salavisa 456bdcd307 fix: synchronous event bus dispatch with regression tests (#106) 2026-01-27 22:55:30 +00:00
João Salavisa 0b573bbdda feat(workflow): implement workflow system with steps terminology (#102) 2026-01-27 01:21:47 +00:00
Carlos Florêncio 7c82ccdf24 feat: improve logging (#105) 2026-01-27 00:18:42 +00:00
João Salavisa d43037bf82 fix: filter upstream commits, fix ahead/behind, and remove duplicate types (#91) 2026-01-25 00:16:01 +00:00
João Salavisa 39b6e89244 feat: Git status tracking refactor with persistent snapshots and commit history (#86) 2026-01-24 00:41:43 +00:00
Carlos Florêncio 752e6b4655 feat: process runners (#71) 2026-01-22 16:39:13 +00:00
João Salavisa 89d7f10211 feat: add session turns with duration display and live timer (#72) 2026-01-21 23:35:29 +00:00
João Salavisa dea2a07cfa feat: add context window usage display to task session (#66)
* feat: add context window usage display to task session

- Add TokenUsageDisplay component showing context window usage as a horizontal progress bar
- Add useSessionContextWindow hook with proper Zustand reactivity (subscribe to primitives)
- Add ContextWindowState to store with setContextWindow action
- Backend: propagate context_window data through orchestrator, watcher, and lifecycle events
- WebSocket handler extracts context_window from session.state_changed metadata
- Display positioned on left side of action bar, updates in real-time

* refactor: remove agent stream event WebSocket broadcasting

Agent stream events (message_chunk, tool_call, etc.) are now handled
internally by the lifecycle manager rather than being broadcast to
the frontend. Messages are accumulated and flushed as comments at
step boundaries.
2026-01-21 02:47:20 +00:00
João Salavisa ecff207765 fix: refactor shell streaming to use event bus pattern (#50) 2026-01-19 23:47:35 +00:00
Carlos Florêncio 8b8f8ff20b feat: refactor main, add providers (#46)
* feat: add providers

* feat: further refactor internal packages
2026-01-19 21:26:29 +00:00
João Salavisa 9ee0052a6b feat(events): add typed event payloads for event bus messages (#32) 2026-01-18 00:25:48 +00:00
João Salavisa dd68da4275 refactor: unify permission requests into agent event stream (#31) 2026-01-17 22:21:10 +00:00
Carlos Florêncio fb042e4c59 feat: session refactor (#30) 2026-01-17 19:02:22 +00:00
Carlos Florêncio 4560901d35 feat: flaky resume sessions 2026-01-15 23:40:20 +00:00
Carlos Florêncio 712610e428 feat: rename agent_sessions to task_sessions 2026-01-15 20:31:39 +00:00
Carlos Florêncio e1bce95bac feat: golang linter 2026-01-15 19:05:16 +00:00
Carlos Florêncio 7d0905b8cb feat: tasks refactor 2026-01-15 18:25:42 +00:00
João Salavisa af7fb84a53 fix: address code review issues (#16) 2026-01-14 23:12:12 +00:00
João Salavisa 159f98efe6 feat(web): File browser with syntax-highlighted viewer (#13)
* feat(web): add file browser with syntax-highlighted viewer

- Add file browser component in right panel to browse workspace files
- Open files in left panel tabs alongside Notes/Changes/Chat
- Color-coded dots for file types (TS, JS, Go, Python, etc.)
- Visual separator between main tabs and file tabs
- Folders sorted to top in file tree view
- LRU eviction for max 4 open file tabs
- Backend handlers for workspace file tree and content via WebSocket

* fix(web): refresh file tree on filesystem changes via fsnotify

- Subscribe to file change notifications from agentctl's fsnotify watcher
- Backend lifecycle manager now connects to file changes stream
- Publishes FileChangeNotified events to event bus
- WebSocket broadcasts workspace.file.changes to task subscribers
- Frontend file browser refreshes tree when changes detected

* perf(agentctl): use fsnotify instead of polling for git status

- Replace 2-second polling with fsnotify-triggered updates
- Add debouncing (300ms) to batch rapid file changes
- Git status updates now triggered by actual filesystem changes
- More efficient and responsive than periodic polling

* refactor(agentctl): consolidate fs events into single debounced trigger

- Single fsChangeTrigger channel for all filesystem events
- After 300ms debounce: update git status, files, and notify subscribers
- Removed per-file notifications in favor of single 'refresh' event
- Simpler and more efficient than separate triggers

* refactor(agentctl): simplify file API - use HTTP for tree/content

- Remove unused filesConn, fileTreeConn, fileContentConn from Client struct
- Convert RequestFileTree and RequestFileContent from WebSocket to HTTP GET
- Remove StreamFiles and CloseFilesStream (redundant with file changes stream)
- Keep only fileChangesConn for the debounced refresh notification

HTTP is simpler than WebSocket for request/response patterns.

* feat(web): switch to CodeMirror for file viewer

- Replace react-syntax-highlighter with @uiw/react-codemirror
- Add language extensions for JS/TS, Python, Go, Rust, Java, C++, CSS, HTML, JSON, Markdown, YAML
- Use VS Code dark theme for consistent look
- Enable code folding and read-only mode
- Better performance and features than syntax highlighter

* refactor: consolidate duplicated types and extract shared constants

- Re-export file types from process package in agentctl/client.go instead of duplicating
- Move OpenFileTab type to lib/types/backend.ts (shared by file-browser and page-client)
- Extract FILE_EXTENSION_COLORS constant for file type color indicators
- Remove console.log statement and unused imports in file-browser.tsx
- Add 'refresh' operation to FileChangeNotificationPayload type

* refactor(agentctl): move Close() method from acp.go to client.go

Close() is a general client cleanup method that closes all streams
(ACP, permissions, git status, file changes). It belongs in client.go
since acp.go should only contain ACP-specific communication logic.

* fix(web): remove unused cn import in file-browser

* refactor(agentctl): consolidate duplicated types into process/types.go

- Create process/types.go with all workspace-related types
- Remove duplicate GitStatusUpdate, FileInfo from workspace_tracker.go
- Remove duplicate GitStatusUpdate, FileInfo from api/workspace.go
- Re-export all types from agent/agentctl/client.go for convenience

Types are now defined once and imported everywhere.

* chore: remove package-lock.json, add to .gitignore

Project uses pnpm (pnpm-lock.yaml), not npm.

* fix(orchestrator): probe agent before returning stale execution state

When checking task.execution, the backend now probes the actual agent:
- For tracked instances: health check via agentctl
- For Docker: verify container is running

If the agent is not running but DB says 'running', the session is
marked as stopped and false is returned. This fixes the UI showing
agents as running after backend restart when they're actually dead.

Changes:
- Add IsAgentRunningForTask to AgentManagerClient interface
- Add IsContainerRunning to docker client
- Update GetExecution to verify agent is actually running
- Clean up stale sessions in database when agent not found

* fix(orchestrator): use agentctl status to verify agent is running

Simplified approach - probe agentctl's /api/v1/status endpoint to check
if agent process is running. Same check for both Docker and standalone.

- Add IsAgentRunning() helper to StatusResponse
- Simplify IsAgentRunningForTask to just use agentctl.GetStatus()

* fix(orchestrator): also verify agent running for in-memory executions

The previous fix only checked agent status when loading from DB. But if
an execution is cached in memory and the agent process stops (while
agentctl stays running), the stale state was still returned.

Now both paths verify the agent is actually running by probing agentctl.
If agent stopped, clean up in-memory state and update DB session.

* chore(web): remove unused react-syntax-highlighter

Switched to CodeMirror for code viewing, no longer needed.

* fix(tests): add IsAgentRunningForTask to mock agent managers

Add the new interface method to test mocks:
- mockAgentManager in scheduler_test.go
- SimulatedAgentManagerClient in orchestrator_test.go

---------

Co-authored-by: Kandev Agent <kandev@example.com>
2026-01-14 01:24:05 +00:00
Carlos Florêncio aebcf5283f feat: display settings in kanban page 2026-01-13 22:37:36 +00:00
Carlos Florêncio f2aea364af feat: environments and executors 2026-01-13 00:28:25 +00:00
João Salavisa a1013342a3 feat: Real-time Git Status Integration (#8)
* feat: implement real-time git status integration

- Add workspace tracker to monitor git changes every 2 seconds
- Implement WebSocket streaming for git status updates
- Add database persistence for git status in agent session metadata
- Compute diffs backend-side with additions/deletions per file
- Integrate git status into frontend UI (top bar, files panel, changes panel)
- Display real-time ahead/behind commits and line changes
- Show file diffs with syntax highlighting in task view
- Add retry logic for WebSocket connections during reconciliation
- Fix context lifecycle issue in workspace tracker

* refactor: remove debug and unnecessary logging

- Remove debug logs from workspace tracker
- Remove info logs from WebSocket handlers
- Remove verbose git status update logs
- Keep only essential error logs

* fix: compare git diff against remote branch instead of HEAD

- Changed git diff comparison from HEAD to remote tracking branch
- Now shows all changes including committed files that haven't been pushed
- Files committed locally but not pushed will appear in the diff
- Falls back to HEAD if no remote branch is configured

* fix: prevent stale git status when switching tasks

- Clear git status when switching to a new task
- Only accept git.status WebSocket updates for the current task
- Prevents showing git data from previous task when switching

* refactor: remove unused diff stream client code

- Remove diffConn field from agentctl Client struct
- Remove StreamDiff() method (never called)
- Remove CloseDiffStream() method
- Remove DiffUpdate type from client.go
- All diff data is already included in GitStatusUpdate.Files
- Keep server-side diff endpoint in agentctl for potential future use

* refactor: remove unused diff stream server-side code

- Remove /workspace/diff/stream endpoint from agentctl API
- Remove handleDiffStreamWS handler
- Remove DiffUpdate type from workspace_tracker.go
- Remove DiffSubscriber type
- Remove updateDiff() and getDiff() methods
- Remove SubscribeDiff(), UnsubscribeDiff() methods
- Remove notifyDiffSubscribers() method
- Remove GetCurrentDiff() method
- Remove currentDiff field and diffSubscribers map
- All diff data is already included in GitStatusUpdate.Files

* refactor: remove unused output stream code

- Remove /output and /output/stream endpoints from agentctl API
- Remove handleGetOutput and handleOutputStreamWS handlers
- Delete output.go file entirely
- Remove OutputBuffer and related code (buffer.go deleted)
- Remove outputConn field from agentctl Client
- Remove StreamOutput() and CloseOutputStream() methods
- Remove OutputLine type from client.go
- Remove OutputBufferSize config option
- Update readStderr to just log instead of buffering

Output data flows through ACP stream (StreamUpdates) which includes
all session notifications. The separate output stream was never used.
2026-01-12 23:44:29 +00:00