61acc82b740248550a314d8c3810ad34a6e70ccf
20 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
72e3a49718 | fix(watcher): self-heal orphaned watchers when agent profile is soft-deleted (#1094) | ||
|
|
d66d9e6f67 | feat: remote tab in task-create dialog — multi-row GitHub repo picker (#1116) | ||
|
|
b8a09bc5ea |
fix(github): pick allowed merge method so squash-only repos stop 405ing (#999)
* fix(github): pick allowed merge method so squash-only repos stop 405ing The merge button sent an empty merge_method to GitHub, which defaults to "merge" regardless of repo settings — squash-only / rebase-only repos returned 405 "Merge commits are not allowed on this repository". The backend now resolves an empty method to the first allowed one via a new GetRepoMergeMethods endpoint (5-min cache), and the frontend renders a GitHub-style split button so users can pick among the allowed methods. * fix(github): address PR #999 review feedback - pr-merge-button: extract repo-methods fetch to useRepoMergeMethods hook (Claude bot); hide button while methods are loading so the primary label doesn't flicker (Claude bot); drop e.preventDefault on dropdown items so Radix closes the menu on selection (Greptile P1). - gh_client / pat_client: treat missing allow_* fields as false instead of true so a permission-gated response can't pick a disallowed merge method (cubic P2). - service.MergePR: only overwrite mergeMethod when pickDefaultMergeMethod returns non-empty, so a degenerate all-false config falls through to GitHub's default with the real error (Claude bot). * fix(github): don't hide merge button on lookup failure Round-2 PR feedback (Claude, Greptile, CodeRabbit, cubic): the `if (!methods) return null` guard added in the previous round to avoid label flicker turned a transient repo-methods lookup failure into a permanent UI lockout — the button would stay hidden until unmount/remount. - pr-merge-button: revert the null-guard; treat null as "use backend resolver", render the plain primary button without a dropdown. - use-repo-merge-methods: add a 5-min frontend TTL to match the backend cache so admin merge-strategy flips are visible without a page reload. Catch block now triggers a rerender, and co-waiting consumers attach both fulfilled/rejected handlers to avoid silent rejection swallowing. * test(github): cover useRepoMergeMethods + silence rejection warning Round-3 PR feedback (Claude): - Swallow rejections in the originating fetch chain instead of rethrowing, so the first consumer's promise never escapes the hook as an unhandled rejection. Coalesced consumers already re-render to null, which is the same state a fresh "not loaded" call would show. - Add use-repo-merge-methods.test.ts covering: null-on-missing-params, loading then resolution, cache hit on remount, concurrent-fetch coalescing, retry-on-failure (no cache poisoning, no unhandled rejection), and TTL-expiry refetch via fake timers. * test(github): cover httpGetRepoMergeMethods error branches Round-4 PR feedback (Claude): the new endpoint had only happy-path coverage. Add tests matching httpMergePR's existing shape: - nil client → 503 (ErrNoClient) - GitHubAPIError{404} → 404 - generic upstream error → 500 --------- Co-authored-by: Kandev Agent <agent@kandev.dev> |
||
|
|
1f264adbc4 | fix(github): drain piled-up PR/issue review tasks via cleanup policy + manual sweep (#929) | ||
|
|
14c19ea538 |
feat(web): add CI hover popover on PR top-bar button (#846)
* feat(github): plumb CI popover fields + branch protection cache
Adds RequiredReviews/UnresolvedReviewThreads/ChecksTotal/ChecksPassing
to TaskPR + PRStatus. Extends the GraphQL PR query with reviewThreads
to populate UnresolvedReviewThreads on every poll. New branch_protection.go
holds an in-memory TTL cache + BranchProtectionFetcher interface;
PATClient/GHClient implement REST/CLI lookups so SyncTaskPR can resolve
required_approving_review_count for the PR's base branch. SyncTaskPR
preserves existing counts when a lightweight (GraphQL) sync returns 0
so the popover doesn't flap during the polling cycle.
* feat(github): mock controller hooks for CI popover e2e
Extends associateTaskPR to accept review/check counts, required_reviews,
and unresolved_review_threads, and switches to ReplaceTaskPR so re-POSTing
the same task drives the popover live-refresh path. Adds POST /pr-feedback
to seed PRFeedback (checks/reviews/comments) keyed by a deterministic
synthetic head_sha, and PUT /auth-health to flip the mock client's
authenticated flag for the "Reconnect GitHub" branch. Mock client gains
ReplaceCheckRuns/ReplaceReviews/ReplaceComments helpers and an
authenticated field that defaults to true.
* feat(web): CI hover popover on PR top-bar button
Hovering the PR top-bar button now reveals a popover with
Passed/In-Progress/Failed counts, a workflow-rollup list under the
non-empty buckets (with "open on GitHub" + "+ ctx" affordances on
failed rows), a review-state row ("Approved 1 / 2 required"), and
unresolved-comments count. Click on the button keeps the existing
"open PR detail panel" behavior so shipped tests stay green.
PRFeedback is lazy-fetched on hover-open via a new SWR-style hook
backed by a 20-entry LRU cache slice; the popover refetches whenever
the underlying TaskPR's updated_at changes (the WS handler already
applies poll deltas). Bucket logic + workflow grouping live in a
small util with vitest coverage. The aggregate-popover variant for
multi-PR tasks ships but the multi-PR trigger preserves its existing
DropdownMenu so per-row click → addPRPanel still works.
* test(e2e): pr-topbar-popover scenarios A–F covering CI popover
11 specs covering: count rows + zero-bucket hiding, workflow rollup
with (N/M passed) badge under Failed, review row + unresolved comments,
hover open + leave close, failed-row open/+ctx affordances, header
external link, "No checks have started" empty state, "Reconnect GitHub"
auth-lost branch, and live count updates on a re-associate.
Adds api-client helpers (mockGitHubSeedPRFeedback, mockGitHubSetAuthHealth,
extended mockGitHubAssociateTaskPR) and SessionPage popover accessors.
* fix(github): address bot review comments on CI popover
- branch_protection.go: evict stale entries on read so the cache map
cannot grow unbounded over a long-running deployment; rewrite the
comment to drop the inaccurate singleflight claim (no dedup logic
exists, concurrent misses race harmlessly to set()).
- gh_client.go: replace fragile strings.Contains "HTTP 404" with an
isNotFoundErr helper that tolerates the formats current and future
gh CLI versions use ("HTTP 404", "404 Not Found", "status: 404").
- graphql.go: extend reviewThreads with totalCount and attribute the
un-fetched tail to the unresolved bucket so PRs with >100 threads
don't silently undercount in the popover.
- pr-ci-popover.tsx: drop the dead "icon" initializer in PRReviewRow
(every branch overwrites it). Memoize useAddCheckToContext so
PRWorkflowRow's reference equality stays stable across renders.
- pr-topbar-button.tsx: collapse the duplicate JSDoc on
usePopoverInteractions.
* fix(github): address remaining bot review comments + tests
Real correctness fixes:
- Replace the "0/0 means counts omitted" heuristic with an explicit
PRStatus.ChecksPopulated flag so the rich REST sync can clear stale
totals when checks are removed from a PR upstream.
- Refresh tp.BaseBranch from status.PR on each SyncTaskPR so a
retargeted PR's branch-protection lookup keys off the new branch.
Persist base_branch in UpdateTaskPR.
- Cache 403 from /branches/.../protection (token lacks Administration:
Read scope) the same way as 404 — otherwise every poller tick burns
rate-limit quota retrying. Both PATClient and GHClient.
- usePRCIPopover: refetch on every popover-open transition (not just
on updated_at change), via a wasEnabledRef gate. Restores the
intended SWR semantics that the lastSyncedRef-only guard suppressed.
- groupChecksByWorkflow: prefer the URL of a job matching the group's
bucket so clicking a "failed" workflow row navigates to a failing
job, not an earlier passing one.
Tests:
- branch_protection_test.go: cache hit/miss/TTL/eviction, error
not cached, no-rule cached, client-without-fetcher path.
- pat_client_test.go: TestPATClient_FetchBranchProtection table-test
for 200/200-with-null/404/403/500.
- service_test.go: ChecksPopulated preserve-on-zero, accept real
zero, base-branch retarget, change-publish for the new fields.
* refactor(github): trim associateTaskPR + drop unreachable PRAggregatePopover
- mock_controller.go: extract associateTaskPRRequest struct,
buildTaskPRFromRequest, and ensureMockPRForRequest helpers so the
handler is back under the 80-line Go limit.
- pr-ci-popover.tsx: remove unreachable PRAggregatePopover export.
Multi-PR tasks currently use the existing DropdownMenu trigger
(no hover popover), so the aggregate component was dead code.
* fix(github): drop unused anchorRef + tests for is{NotFound,Forbidden}Err
- pr-topbar-button.tsx: anchorRef was leftover from the dropped
click-to-pin onInteractOutside guard. Removed.
- gh_client_test.go: tests for the new isNotFoundErr / isForbiddenErr
helpers covering each accepted format and a few non-matches so a
future gh CLI wording change is caught.
* fix(github): UnresolvedReviewThreadsPopulated guard + drop unreachable nil checks
- service.go: mirror the ChecksPopulated/preserve dance for
UnresolvedReviewThreads. The REST single-PR poller (getPRStatus)
doesn't fetch review threads, so without this guard every REST
sync would clobber the GraphQL-populated value to 0 and emit a
spurious github.task_pr.updated event. The GraphQL path
(convertBatchedPRResult) sets the new flag.
- branch_protection.go: drop unreachable s == nil / fetcher == nil
checks. fetchRequiredReviews is only invoked from SyncTaskPR on a
service constructed via NewService, which always initialises the
cache. After a successful type assertion, the interface value
cannot be nil; the !ok branch already covers nil-interface inputs.
- service_test.go: new TestSyncTaskPR_UnresolvedThreads_PreservesOnLightweightSync
covers the regression. Existing NewFieldsPublishEvent threads-change
case updated to set UnresolvedReviewThreadsPopulated.
* refactor(web): drop CI popover progress bar — last-updated is enough
The animated progress bar was visual noise; the "updated Ns ago"
timestamp tells the user the same thing without the indicator. Footer
also hides entirely until the first fetch lands so an empty popover
doesn't show a useless "" line.
* fix(web): background-sync PRFeedback so popover is fresh on first hover
The on-hover SWR fetch was the only refresh path, so opening the popover
showed the previously-cached value while a new fetch raced behind it —
visibly stale until the refetch landed. The button icon, by contrast,
updates the moment a github.task_pr.updated WS push lands.
Add a usePRFeedbackBackgroundSync hook mounted at the PRTopbarButton
that subscribes to the same pr.updated_at change signal and refetches
PRFeedback into the cache in the background. The popover hook still
fires an on-open refetch as a safety net for the very first hover
(before any sync). No extra polling — fetches are gated on real
TaskPR changes, matching the icon's cadence.
* feat(web): CI status chip in chat status bar + tweak elapsed label
- New <PRStatusChip> renders a compact colored circle in the chat
status bar (left of the TodoIndicator, just above the input). Hover
opens the same PRCIPopover anchored to the chip's top edge so the
card expands upward — matching the user's "open bottom-up" ask. The
chip mounts the background-sync hook so the popover is fresh on
first hover even when the top-bar PR button isn't visible.
- elapsedLabel: "just now" → "updated just now" so the footer reads
the same regardless of how recently the cache was refreshed.
* fix(web): chip color was missing — use static class map for Tailwind JIT
\`getPRStatusColor().replace("text-", "bg-")\` produced class strings at
runtime, which Tailwind's content scanner doesn't pick up — the chip
rendered with no background. Replace with a static lookup so each
literal `bg-*` class appears in the source and gets compiled into the
bundle.
* fix(web): chip uses currentColor SVG so the dot actually renders
The previous bg-* approach relied on Tailwind compiling the literal
class strings. Even with a static map some palette entries (notably
bg-muted-foreground) didn't surface as a usable utility, so the chip
remained transparent. Apply getPRStatusColor's text-* class to the
button wrapper and fill an SVG circle with currentColor — the text-*
utilities are already compiled (PRTaskIcon uses them) so the dot
is guaranteed to render in the right color.
* fix(web): redesign PR status chip — clipboard icon + status glyph
- Move chip to right of TodoIndicator (was left).
- Render as "[clipboard icon] [status glyph]" instead of a bare circle:
passed → green CircleCheck, failed → red CircleX, in_progress →
yellow Loader2 (spinning), merged/neutral → muted dot. Status icons
reuse already-compiled text-* utilities so colors render reliably.
* fix(github): ReviewCount = approved-reviewer count + ReviewCountsPopulated guard
Two bugs feeding the popover's "Approved (0)" misreport:
- Semantic: REST path set ReviewCount to len(reviews), which double-counts
authors who left multiple reviews and includes COMMENTED rows. Replace
with countApprovedReviewers — distinct authors whose latest review
state is APPROVED. GraphQL converter gets the parallel
countApprovedReviewerNodes helper and also populates PendingReviewCount
from ReviewRequests.TotalCount (was unset, so the chip's "(K requested)"
suffix was always missing).
- Clobber: GraphQL polls used to write ReviewCount/PendingReviewCount=0
on every cycle because the converter never set them. Add
ReviewCountsPopulated to PRStatus and gate SyncTaskPR's writes on it
the same way ChecksPopulated already does, so a partial sync path
preserves the persisted values instead of resetting them to zero.
* fix(web): popover nitpicks (icon, review row, indent, badge)
- Chip icon: ClipboardCheck → ListCheck.
- PRReviewRow always renders (drop the early-return). Fresh PRs with
no approvals + no protection rule now show "Awaiting review" instead
of vanishing.
- Indent workflow rows under the In Progress / Failed group headers
(pl-5) so the hierarchy reads more clearly.
- In-progress workflow badge: "0/1 ran" → "1 running". The old phrasing
hid whether the job had even started.
* feat(web): stacked pass-rate progress bar in CI popover
Mirrors TodoIndicator's progress bar at the top of the CI section, but
split into three coloured segments:
green (passed) | yellow (in_progress) | red (failed)
sized proportionally to each bucket's count. The label reads
"Pass rate · K/N (P%)" so the user gets the same at-a-glance summary
TODOs shows. Skipped/cancelled-but-not-failed jobs aren't counted —
the bar always reflects the actionable picture.
* fix(web): right-align review counts + drop chip hover bg
- Move approval/required counts to the right edge of the review row
("Approved" on the left, "1 / 2" on the right), mirroring the
bucket-row layout. The "(K requested)" suffix becomes a "· K requested"
trailing fragment on the right side.
- PRStatusChip: drop hover:bg-accent/50 so the trigger doesn't grow a
background plate when the popover is open.
* fix(web): switch CI chip icon to IconChecklist
* fix(github): clear bot review-comment backlog
Real correctness fixes:
- pr-status-chip: pending checks / pending review now beat
checks_state === "success" in chipStatus precedence — a PR with
CI green but reviewers outstanding is no longer rendered as
"passed".
- branch_protection: drop the unreachable Reset() method.
- pr-topbar-button: stop double-mounting usePRFeedbackBackgroundSync;
the chip already covers it.
- use-pr-ci-popover: dedupe key combines prFeedbackKey + updated_at
so switching tasks with same-timestamp PRs still refetches.
- use-pr-ci-popover: drop stale comment about the removed progress bar.
Refactor:
- client_helpers + graphql: extract reviewSample +
latestReviewStateByAuthor helper so the three near-identical
per-reviewer dedup loops share one implementation.
Tests:
- countApprovedReviewers + reduceReviewSummary unit tests.
- TestSyncTaskPR_ReviewCounts_PreservesOnLightweightSync +
AcceptsRealZero mirror the existing ChecksPopulated guards.
- groupChecksByWorkflow distinct-URL test verifies the failed-job
URL-selection fix.
- branch_protection TTL tests now use testing/synctest — instant
virtual time, no real-sleep flake on CI.
* fix(web): clicking the CI chip no longer closes the popover
HoverCard treats the trigger button as outside the content's bounding
box, so a click on the chip fired Radix's onPointerDownOutside and
auto-closed the popover. Add a triggerRef + onPointerDownOutside guard
on HoverCardContent that filters out clicks landing on the trigger —
clicking the chip is now a no-op while the popover stays open via hover.
|
||
|
|
b1cd2d2076 | feat(github): rate-limit awareness, poller throttling, and GraphQL batching (#821) | ||
|
|
a510b46e4e |
feat(multi-repo): support tasks spanning multiple repositories (#767)
* feat(task): add task_environment_repos schema and model for multi-repo support
Phase 0 of multi-repository task support: introduces the per-repository
child table and model alongside legacy single-repo fields (dual-write,
no behavior change). Lays the foundation for the orchestrator/lifecycle
refactor in subsequent phases.
- New task_environment_repos table with FK CASCADE on task_environments.
- New task_dir_name column on task_environments (idempotent ALTER).
- TaskEnvironmentRepo model + Repos slice + RepoFor helper on TaskEnvironment.
- CRUD methods + interface extension; env getters preload Repos.
- Idempotent backfill from legacy single-repo task_environments rows.
- Tests cover round-trip, CRUD, preload ordering, embedded create, cascade
delete, and backfill from a legacy-shaped row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(lifecycle): add Repositories list to LaunchRequest and EnvPrepareRequest
Phase 1.1 of multi-repository task support: extends the lifecycle types
with explicit per-repo lists, while keeping the legacy single-repo
top-level fields as the synthesized fallback. No caller behavior changes
yet — preparers still see length-1 specs for single-repo launches.
- LaunchRequest.Repositories []RepoLaunchSpec + RepoSpecs() helper.
- EnvPrepareRequest.Repositories []RepoPrepareSpec + RepoSpecs() helper.
- EnvPrepareResult.Worktrees []RepoWorktreeResult for per-repo outcomes.
- runEnvironmentPreparer forwards req.Repositories into the prepare
request, inheriting the request-level RepoSetupScript when each
per-repo entry has none.
- Tests cover explicit-list precedence, synthesized fallback, and
repo-less requests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree,lifecycle): support multiple worktrees per session
Phases 1.2 and 1.3 of multi-repository task support. The pieces are
intertwined so they ship together — the preparer's per-repo loop
depends on the manager being able to hold one worktree per
(session, repository) pair instead of one per session.
Worktree manager:
- In-memory cache key changes from sessionID to (sessionID|repositoryID).
- New MultiRepoStore optional capability + SQLite implementation:
GetWorktreesBySessionID and GetWorktreeBySessionAndRepository.
- New Manager methods: GetAllBySessionID, GetBySessionAndRepo. Existing
GetBySessionID returns the first match (single-repo callers unchanged).
- Manager.Create's "already exists" check is now repo-scoped so the
second repo's create no longer returns the first repo's worktree.
- All cache write/delete sites updated to the composite key.
WorktreePreparer:
- New prepareMultiRepo path activated when EnvPrepareRequest.Repositories
has >=2 entries; single-repo and repo-less behavior is byte-identical.
- prepareOneRepo runs validate -> sync -> create -> fetch-PR -> per-repo
setup-script for each spec; rolls back already-created worktrees on
any failure.
- Workspace path becomes the task root (parent of any repo subdir);
legacy WorktreeID/WorktreeBranch/MainRepoGitDir mirror Worktrees[0]
for downstream code that hasn't been migrated yet.
- Per-repo setup script runs verbatim (no script-engine resolution
yet — the per-repo placeholder namespace lands in a follow-up).
Tests:
- Manager: GetAllBySessionID, GetBySessionAndRepo (hit + miss),
GetBySessionID single-repo compat, cache key separation.
- Preparer: multi-repo creates one worktree per repo; rollback removes
active worktrees on partial failure; per-repo setup script runs in
each worktree directory and emits its own step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): resolve all task repos and persist per-repo rows on launch
Phase 1.4 of multi-repository task support: the orchestrator now reads
every repository linked to a task, forwards the full list through the
adapter into the lifecycle layer, and persists one TaskEnvironmentRepo
plus one TaskSessionWorktree row per repo when the launch is multi-repo.
Single-repo and repo-less tasks keep their previous code path verbatim
(dual-write — legacy single-repo fields stay populated from the primary).
- New resolveAllRepoInfo / resolveTaskRepoInfo on the executor; primary
is the first by Position.
- buildLaunchAgentRequest takes both the primary repoInfo and the full
list, populating req.Repositories []RepoSpec only when length > 1.
- New RepoSpec / RepoWorktreeResult on the orchestrator's executor
package mirror the lifecycle types so the adapter can translate
between them without leaking lifecycle types into the orchestrator
public API.
- Adapter (cmd/kandev/adapters.go) forwards req.Repositories into
lifecycle.LaunchRequest and surfaces per-repo prepare results back
on LaunchAgentResponse.Worktrees.
- persistTaskEnvironment writes env.Repos in the same create
transaction; persistTaskEnvironmentRepos handles the existing-env
reuse path. TaskEnvironment.TaskDirName is now persisted.
- persistWorktreeAssociation loops over resp.Worktrees in multi-repo,
inserting one row per repo with its own RepositoryID.
- ResumeSession now also populates req.Repositories from task repos
and stamps the existing TaskDirName so the preparer reuses the same
task root.
- repoStore / executorStore interfaces extended with the new repo and
env-repo CRUD methods; mocks updated accordingly.
Tests:
- LaunchPreparedSession with two task repositories populates
req.Repositories in order and keeps legacy single-repo top-level
fields filled from the primary.
- The same launch persists one TaskEnvironment + 2 TaskEnvironmentRepo
+ 2 TaskSessionWorktree rows.
- Single-repo launch still produces zero entries in req.Repositories
and exercises only the legacy fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): git endpoints accept optional repo subpath for multi-repo
Phase 2 of multi-repository task support: every per-call git endpoint
gains an optional `repo` field (form param for GET, JSON for POST) that
selects a sub-directory of the workspace. Empty `repo` = workspace root,
preserving single-repo behavior verbatim.
- Manager.GitOperatorFor(subpath) returns a per-subpath GitOperator
(cached). Validates the subpath against path traversal, absolute
paths, and missing directories.
- New api server helper gitOpForRepo() emits a 400 with a per-operation
GitOperationResult on invalid subpaths.
- Repo field added to: pull, push, rebase, merge, abort, commit,
rename-branch, stage, unstage, discard, create-pr, revert-commit,
show-commit, reset, log, cumulative-diff. (git/status still reads
from the workspace tracker — that endpoint will gain multi-repo
support when the tracker itself is split per repo.)
- Tests cover empty subpath, valid subpath caching, traversal
rejection, and missing-directory rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/comments): scope diff comments by repository for multi-repo tasks
Phase 3 of multi-repository task support: comments now carry an
optional repositoryId so two repos with the same file path render
their own comment threads instead of bleeding together.
- CommentBase.repositoryId (optional, backwards compatible).
- getCommentsForFile gains an optional repositoryId arg; legacy
comments without a repositoryId match any repo filter so persisted
data keeps showing.
- Diff store interface in lib/diff/types.ts mirrors the new arg.
- Tests cover per-repo filter, omitted-arg fallback, and legacy data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(github): per-repo PR association on task PRs and watches
Phase 4 of multi-repository task support: github_task_prs and
github_pr_watches gain a repository_id column so multi-repo tasks can
have one PR per repo without colliding on (task_id) uniqueness.
- Idempotent ALTER TABLE adds repository_id to both tables.
- TaskPR.RepositoryID and PRWatch.RepositoryID model fields.
- New GetTaskPRByRepository and ListTaskPRsByTask lookups; existing
GetTaskPR keeps single-PR semantics for callers not yet migrated.
- ReplaceTaskPR's delete is scoped by repository_id when set so a
multi-repo task replacing one repo's PR no longer wipes the other.
- Tests cover per-repo storage and scoped replace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): foundations for multi-repo task UI
Phase 5 of multi-repository task support: primitives the upcoming UI
work needs without changing any visible behavior yet.
- TaskRepository.checkout_branch surfaces the optional PR head branch
field that the backend already exposes.
- primaryTaskRepository(repos) picks by lowest Position so consumers
match the backend's "primary" semantics instead of `[0]`.
- New useTaskRepositories(taskId) / useTaskPrimaryRepository(taskId)
hooks read the live kanban task and return its repos sorted by
position. Future kanban/diff/UI consumers should use these instead
of poking task.repositories directly.
- Tests cover lowest-position selection and empty/single-repo edge cases.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): kanban card shows multi-repo "+N" chip and hydrates full repo list
Phase 6 (subset) of multi-repository task support. The kanban now
exposes the per-task repository list to the UI and renders a compact
"+N" badge on cards whose tasks span more than one repository.
- KanbanState.tasks[].repositories slim shape (id, repository_id,
base_branch, checkout_branch, position).
- SSR mapper hydrates the full list and uses primaryTaskRepository()
to pick the position-aware primary instead of [0].
- useKanbanData passes repositories through to the card props.
- KanbanCard renders the primary repo name with a small "+N" chip
when len(repositories) > 1; tooltip names the count.
- useTaskRepositories / useTaskPrimaryRepository hooks return the
slim KanbanTaskRepository shape so multi-repo consumers share one
read path.
Larger UI surfaces (task-create dialog, diff view, file browser,
mobile sheets) still target the primary repo only — they will be
ported as the multi-repo task creation flow lands. No visible change
for single-repo tasks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(task): reject duplicate repositories on multi-repo task create
Phase 8 of multi-repository task support: surface duplicate-repository
input as a clean validation error instead of letting the unique
(task_id, repository_id) constraint surface mid-loop with some rows
already inserted.
- createTaskRepositories tracks seen repository IDs and returns
"repository <id> listed more than once for the task" before any
CreateTaskRepository call.
- Tests cover the rejection path and confirm two distinct repos still
succeed with both rows persisted in Position order.
Repository discovery and per-repo cloning were already covered by
Phase 1.4 (resolveAllRepoInfo loops ensureRepoCloned) and the
existing repository_discovery service is repo-list aware by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-create): repeatable repository rows for multi-repo task creation
Users can now actually create a multi-repo task: a "+ Add repository"
button below the primary repo + branch selectors expands the form with
additional (repository, base-branch) rows. Each extra row picks from a
filtered repository list that excludes the primary and any other already-
chosen extras, so duplicates are impossible to enter.
- ExtraRepositoryRow type + form-state hook (extraRepositories, add /
remove / update / reset). Lives in its own file so the main state hook
stays under the file-length lint cap.
- ExtraRepositoryRows component renders the rows and the "+ Add" button;
per-row branches are loaded on demand via the existing
useRepositoryBranches hook.
- buildRepositoriesPayload accepts the extras and appends them after the
primary, dropping rows with empty repository_id silently so an
unfinished row does not block submit. The backend's duplicate-repo
validation (Phase 8) catches anything that slips through.
- DialogFormBody is split: CreateModeSelectors (selectors + extra rows)
and useDialogHandlers move to dedicated files, keeping touched files
under the lint thresholds.
- Tests cover the multi-repo payload (ordering, drop-empty, no-primary
recovery, single-repo unchanged) and the existing
store-multi-repo.test.ts gets named constants to satisfy the
no-duplicate-string rule.
Verified: pnpm exec tsc --noEmit clean, vitest 608/608 green,
eslint --max-warnings 0 clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(agentctl): per-repo workspace tracker for multi-repo git/status
The git/status endpoint now accepts an optional ?repo=<subpath> query
param that selects a sub-directory of the workspace, completing the
per-repo plumbing started in the multi-repo phase. Single-repo callers
keep the workspace-root behavior unchanged.
- Manager.GetWorkspaceTrackerFor(subpath) returns a per-subpath
WorkspaceTracker (lazy + cached). Validation reuses a new
resolveSubpath helper shared with GitOperatorFor, so both code paths
reject path traversal / absolute paths / missing dirs the same way.
- Per-subpath trackers are NOT started (no polling goroutines); the
multi-repo path uses them only for synchronous GetCurrentGitStatus
queries which fall through to getGitStatus when no cache is present.
This keeps per-repo cost cheap.
- handleGitStatus reads c.Query("repo") and routes through the new
resolver, returning a 400 with an error payload on invalid input.
- Tests cover empty subpath (root), valid subpath caching, traversal
rejection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/review): group file tree by repository for multi-repo task changes
The review file tree now renders per-repo top-level nodes when a task's
changes span multiple repositories, so the same file path in two repos
no longer collapses into a single ambiguous entry.
- ReviewFile.repository_id and ReviewFile.repository_name (optional)
identify which repo each file belongs to. Single-repo callers leave
them unset and the tree falls back to the existing flat grouping.
- buildFileTree splits into buildMultiRepoTree (when 2+ distinct
repository_names are present) and buildFlatTree (the original logic).
Repo roots have isRepoRoot=true and a repositoryId for downstream
filters; their children are full path subtrees per repo.
- collapse() skips repo roots so a repo with a single changed file
still shows its repo header.
- ReviewFileTree renders repo roots with a stronger label, a top
border, and an "N files" count badge; data-testid="repo-root-node"
for E2E coverage. File-level rendering is unchanged.
- Tests cover the flat-tree fallback (no repo info, single repo),
multi-repo grouping, repo-root pinning, and same-filename-different-
repos isolation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/task-top-bar): "+N" repo chip on multi-repo task header
The task page top-bar breadcrumb now shows a small "+N" chip next to the
primary repository name when the task spans more than one repository,
matching the kanban-card treatment introduced earlier. Hover surfaces the
exact count via tooltip ("frontend (+2 more repos)").
- resolveTaskProps populates repositoryCount from task.repositories.length.
- TaskTopBar threads repositoryCount through to the new
RepositoryBreadcrumb subcomponent.
- The chip carries data-testid="topbar-extra-repo-count" for E2E
selectors.
- TopBarLeft was nearing the function-length / complexity caps; the
breadcrumb extraction restores headroom for future additions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web/task-create): disable "Add repository" when no other repos available
When the user only has the primary repository in their workspace,
clicking "+ Add repository" used to insert a row whose dropdown was
empty — looking like a bug ("no other repos show up"). The button now
disables itself when there are no remaining workspace repositories to
choose from, with a tooltip explaining why ("All workspace repositories
are already added"). The previous "primary not selected" guard is also
surfaced via tooltip.
- ExtraRepositoryRows computes remainingCount = repos − used (primary
+ already-added extras) and gates the Add button on it.
- New computeAddButtonHint helper returns a per-state title string for
the disabled cases.
- Tests cover: dropdown shows the unused workspace repos when opened
(the click path the user reported wasn't actually broken — the gate
was just missing); Add button disabled when only the primary exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(agentctl): workspace tracker falls back to first repo subdir for multi-repo task roots
The "Changes" panel was empty for multi-repo tasks because the
agent's workspace path is the task root — a plain holder directory
that holds one git worktree per repository as siblings. The workspace
tracker tried to run git commands against that bare root and silently
failed, so no git status updates ever reached the UI.
The tracker now detects this at construction: when the configured
workDir isn't itself a git repo, it falls back to the first immediate
child directory that is. The user gets the primary repo's changes in
the panel; cross-repo aggregation is a follow-up.
- preferGitRepoChildIfRootIsBare scans os.ReadDir entries (skipping
hidden / non-directory entries) and returns the first that has a
valid git index. Logs an info line when the rewrite happens so it's
visible in the agentctl log.
- Repo-less tasks (e.g. quick chat) and single-repo tasks keep their
previous behavior verbatim — the fallback only fires when the root
itself fails resolveGitIndexPath.
- Tests cover: fallback when the workdir is a multi-repo holder,
no-fallback for a real git repo, no-fallback when no git child
exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo workspace tracking — Changes panel shows all repos
The agent's workspace tracker is now split per-repository for multi-repo
task roots. Each repo gets its own tracker that polls git independently
and stamps its repository name onto every emitted event. The Changes
panel aggregates files from every repo and groups them under per-repo
headers using the file-tree machinery from Phase 6.
Backend (agentctl):
- GitStatusUpdate / FileListUpdate gain a RepositoryName field. Empty
for single-repo workspaces (legacy clients see no change).
- WorkspaceTracker carries repositoryName and stamps it onto every
emitted status/file-list. Two constructors: NewWorkspaceTracker for
the legacy single-repo path (with the bare-root fallback) and
NewWorkspaceTrackerForRepo for explicit per-repo trackers.
- Manager scans direct child directories for git repos at construction.
When 2+ repos are found, builds one tracker per repo subdir on top of
the root tracker (which stays bound to the bare task root and emits
nothing useful). Single-repo workspaces are unchanged.
- Manager.SubscribeWorkspaceStream / UnsubscribeWorkspaceStream fan out
one client subscription across the root + all per-repo trackers via
AttachWorkspaceStreamSubscriber / DetachWorkspaceStreamSubscriber on
the tracker. The WS handler switched from the single-tracker subscribe
to the manager fan-out.
- Tests cover: per-repo trackers spawn for multi-repo task roots,
single-repo doesn't, both repos emit GitStatusUpdates tagged with
RepositoryName.
Frontend:
- GitStatusEntry / GitStatusData carry repository_name (optional).
- session-runtime gitStatus.byEnvironmentRepo new map keyed by env id
then repository name; setGitStatus routes per-repo updates into it
while still updating the legacy byEnvironmentId for single-repo
consumers. clearGitStatus drops both maps.
- migrateEnvKeyedData migrates the new per-repo map alongside the
existing keys when sessionId resolves to an environmentId.
- WS git-status handler forwards repository_name from event.status.
- New useSessionGitStatusByRepo hook returns sorted [{repo, status}]
pairs for the active session.
- task-changes-panel mergeReviewFiles iterates statusByRepo when
available, stamping repository_name onto every uncommitted file so
the Changes panel groups them under per-repo headers (using the
ReviewFile.repository_name plumbing from Phase 6).
- Tests: per-repo routing, no overwrite on sibling repo update,
legacy single-repo fallback, clear-on-cleanup.
Verified: pnpm exec vitest run 623/623, eslint --max-warnings 0 clean,
tsc --noEmit clean, make test (backend) clean, make lint 0 issues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers above each repo's files
The Changes panel now renders a sticky "frontend / N files" header above
the diffs from each repository so the user can visually distinguish
where each change lives. Single-repo tasks render unchanged (no header
shown when there's only one unnamed group).
- ReviewDiffList groups its file list by repository_name (preserving the
caller's order within each group), then renders one RepoGroupHeader
per group followed by the existing FileDiffSection list. Header gating
rule: show when len(groups) > 1 OR the single group has a non-empty
repository name.
- Group key uses "repo:path" so a file with the same path in two repos
no longer collides on React's reconciliation.
- groupFilesByRepository + RepoGroupHeader live in a sibling
review-diff-list-groups.tsx file to keep review-diff-list.tsx under
the 600-line lint cap.
- Tests cover: no header for plain (no repo_name) files, one header per
repo for 2+ groups, header shown even for a single named repo, file
paths land under the right group's section.
Verified: vitest 627/627, eslint clean, tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lifecycle): propagate RepositoryName through PublishGitStatus
The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate
with RepositoryName, but the lifecycle PublishGitStatus translation
into GitEventPayload was dropping that field. The orchestrator then
forwarded a status with empty repository_name, the frontend store
routed everything into the empty-key bucket, and the Changes panel's
multi-repo grouping never triggered — no header, no per-repo split.
- GitStatusData gains RepositoryName (omitempty so single-repo
payloads stay byte-identical on the wire).
- PublishGitStatus copies update.RepositoryName onto the payload.
- Test asserts the field survives the publish → subscribe round trip
via the in-memory event bus on the GitEvent.<sessionID> subject.
Restart the agentctl process for any running multi-repo task to pick
up the new field; new launches get it automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web/changes-panel): per-repo group headers in the dockview Changes tab
The Changes tab in the dockview panel (the Files / Changes (N) tab the
user actually interacts with — not the standalone Review dialog) was
still rendering a flat file list, so multi-repo task changes either
collided onto the legacy single-status (showing only one repo's files)
or rendered without grouping.
- FileInfo gains repository_name (optional, single-repo unchanged).
- useSessionGit aggregates files from useSessionGitStatusByRepo when
multi-repo data is present, stamping each FileInfo with its
repository_name. Falls back to the legacy single status for
single-repo workspaces. Aggregation extracted into
aggregateFilesAcrossRepos to keep useSessionGit under the 100-line
function lint cap.
- ChangedFile (changes-panel.tsx + changes-panel-timeline.tsx) carries
repositoryName through to the renderer.
- FileListSection delegates to a new FileListBody that groups rows by
repositoryName; renders a small per-repo subheader when 2+ repos OR
any single named repo is present, otherwise falls back to the
pre-multi-repo flat list (same DOM shape).
- Tests cover: no header for plain (no repo_name) files, headers
per repo for 2+ groups, single named repo still gets a label.
Restart the backend so the per-repo trackers spawn for in-flight
multi-repo tasks; new launches are unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): per-repo git ops, unified branch endpoint, chip-style task create
Bundles a string of multi-repo fixes and refactors that came up while
testing the feature end-to-end:
- workspace_path stored on multi-repo TaskEnvironment was one level too
high, so agentctl booted with the wrong WorkDir on cold start; fixed
in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession.
- Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo`
through agentctl client → WS handler → frontend hook → SessionGit
fan-out, so the right git repo is targeted (was running at the task
root, which isn't a git repo for multi-repo tasks).
- agentctl GitLog is multi-repo aware: when no `repo` is specified and
per-repo trackers exist, fans out one log per repo and merges sorted
newest-first with RepositoryName stamped on each commit.
- Per-repo grouping for files (Changes panel) and commits, with
collapsible group headers; commits section defaults to collapsed.
Algorithm extracted into one generic groupByRepositoryName helper
(was copy-pasted three times).
- Task creation dialog rewritten around a unified `fs.repositories[]`
list — no more "primary vs extras" split. Chip-style row with one
pill per repo + branch, "+" to add, x to remove, GitHub URL toggle
inline. Discovered (on-machine) repos appear in any chip's dropdown.
Removes ~6 fields and 3 helpers from the dialog state.
- Branch listing endpoints collapsed to one:
GET /workspaces/:id/branches?repository_id=X | path=Y
Frontend collapses to a single useBranches hook with a tagged-union
source. Drops two HTTP routes, two service methods, two API client
funcs, and one whole hook file.
Tests added across the chain: workspace_path resolver, sort-commits-
by-date, manager poll-mode propagation, RepoSubpaths, chip grouping
+ collapse + per-repo isLatest, group-by-repo helper, dialog payload
builder + chip wiring + discovered-repo flow.
* feat(multi-repo): unified per-repo git controls for all VCS operations
Changes panel and top-bar VCS button render per-repo Stage all, Commit,
Unstage all, Push, Pull, Rebase, Merge, and Create PR uniformly across
single-repo and multi-repo workspaces. Operations route to the correct
git repo subpath; fan-outs collect per-repo results and surface partial
success in the toast layer.
Backend:
- Stamp RepositoryName on Reset/BranchSwitch/FileChange notifications
and forward through orchestrator handlers
- Surface per-repo errors from handleGitLogMultiRepo via PerRepoErrors
Frontend:
- pull/push/rebase/merge/abort/commit fan out via PerRepoOperationResult
- Per-repo group headers always render with action buttons
- Top-bar dropdown lists per-repo Pull/Rebase/Merge entries
- pendingStageFiles keyed per-repo to prevent cross-repo clears
- useRepoDisplayName distinguishes loading from multi-repo
- Commit dialog file summary respects stageAll
- New helpers: changes-panel-helpers.ts, changes-panel-repo-groups.tsx,
use-repo-display-name.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(multi-repo): plumb repo through file/commit/diff endpoints; per-repo PRs and review UI
- backend: thread `repo` subpath through pull/push/rebase/merge/abort, file
content/update, cumulative-diff, and commit-show endpoints; per-repo
fan-out for log + cumulative-diff with each repo's own merge-base; emit
full `repositories` array on task.updated WS payload
- github: store/list TaskPRs as []*TaskPR (per-repo); orchestrator/web store
rekey to `Record<taskId, TaskPR[]>`
- web: aggregate PR icon (count badge + worst-of color, multi-PR tooltip);
branch hover card renders one row per repo with each repo's task base
branch; review dialog merges per-repo file maps with composite keys and
propagates repository_name through diff/expand/revert; commit-detail
panel scopes to the owning repo
- kanban card: chip row of repo names (up to 2 + "+N" overflow, tooltip
lists all)
- task-create dialog: gate non-worktree executors to multi-repo only; move
fresh-branch toggle inline next to the repo chips (icon-only, with
title/aria-label)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(multi-repo): scope PR watches/associations and review UI per repository
Schema: add repository_id to UNIQUE constraints on github_pr_watches and
github_task_prs (with idempotent copy-and-rename migration) so one task
spanning N repos can hold one watch/PR per repo without collisions.
Plumb repository_id through CreatePRWatch/EnsurePRWatch/AssociatePRWithTask
/AssociatePRByURL, fan TriggerPRSync across all per-repo watches, and
disambiguate status sync by (task, owner, repo, pr_number) instead of
"first match". Add Repo to wsCreateFile/Delete/Rename and wsCreatePR/
RenameBranch so file and git ops target the right working tree.
Frontend review dialog: switch reviewedFiles, staleFiles, fileRefs,
selectedFile and comment counts to NUL-separated `${repo}\x00${path}`
keys so same-named files in different repos no longer share state.
useTaskPR consumes the new {prs: TaskPR[]} envelope.
Also: snapshot repoTrackers slice before background refresh in
SetWorkspacePollMode to prevent races with Stop(), and key
mergeCumulativeFiles by NUL-separated (repo, path) to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: apply prettier formatting
Unblock CI prettier check on files modified earlier on this branch
that pre-date the current prettier config.
* fix: address PR review feedback and unblock E2E
Backend:
- ListWorkspaceTaskPRs background sweep now uses TriggerPRSyncAll so
multi-repo tasks refresh every per-repo PR (was only the most recently
updated watch). Caught by claude review on #767.
Frontend (no behaviour change):
- Replace literal NUL byte (0x00) in FILE_KEY_SEP and the inline key
builder in use-review-dialog with the explicit escape so the
source is plain ASCII. The runtime separator is unchanged; this just
removes a footgun that GitHub's diff renders as a visible space.
- Show the auto-resolved branch as a small read-only pill next to the
GitHub URL input so the URL-mode dialog surfaces the branch the task
will run on (previously only the URL was visible).
E2E:
- changes-panel-focus / pr-detection / pr-switcher-changes: expand the
commits-section before asserting on commit text -- the section starts
collapsed since the chip-style refactor.
- repository-selector-scroll: open the popover via repo-chip-trigger
instead of the legacy single repository-selector testid.
* test(e2e): align task-create dialog tests with chip refactor
Updates three more tests that broke when the dialog migrated from a
single repository/branch combobox to per-row repo/branch chips and the
URL-mode toggle relabel:
- create-task.spec.ts (pre-select + remembers-selections): switch
repository-selector / branch-selector lookups to repo-chip-trigger /
branch-chip-trigger.
- create-task-github-url.spec.ts (toggle test): the inverse-state label
is now "use a workspace repository", not the legacy
"or select a repository".
No code changes — purely test alignment with the existing UI.
* fix(web): re-apply workbench-chrome topbar + restore branch-selector behaviour
Two regressions surfaced by the multi-repo merge into main:
1. task-top-bar.tsx had been resolved in our favour at the workbench-chrome
merge (#761), losing the TopbarActionOverflow wiring that the new test
asserts on. Re-applies main's topbar structure (TopbarCluster groups,
TopbarActionOverflow, IntegrationsMenu, MoreToolsMenu, SettingsButton)
on top of our multi-repo additions: repositoryCount + RepositoryBreadcrumb
"+N" chip, gitStatus + isMultiRepo plumbing, and the GitAheadBehindBadges
suppression for multi-repo. GitAheadBehindBadges moved to its own file to
stay under the file-length cap.
2. The chip-style task-create dialog dropped the legacy branch-selector
semantics. Restored:
- Local-executor branch lock: the per-row branch chip is now disabled
when isLocalExecutor and fresh-branch is off. The user's checkout
dictates the branch for local execution; fresh-branch unlocks it.
- Branch override: when locked, the chip shows currentLocalBranch
instead of the value the user picked under a different executor
(so worktree -> local doesn't display a stale "develop" pick).
- Branch refresh: useBranches now exposes a refresh() callback for
id-based sources (calls listRepositoryBranches with refresh=true),
and Pill renders BranchRefreshButton in its popover header when
onRefresh is wired.
- URL-mode branch picker: GitHubUrlSection now renders a real
branch-chip-trigger Pill backed by fs.githubBranches instead of a
read-only span, restoring the "branch selectable in URL mode"
behaviour (URL mode is always remote-clone, so the local-executor
lock does not apply).
E2E spec updated: bulk-rename legacy branch-selector / repository-selector
testids to branch-chip-trigger / repo-chip-trigger.first(). All 13
create-task-branch-selector tests pass locally.
* ci: force workflow re-run
* fix(web): unblock E2E shard 2
Three issues showed up only after the merge with main, which brought back
the E2E shard 2 tests touching git/changes-panel and improve-kandev:
1. Wire the TaskCreateDialog slot props (extraFormSlot, aboveDescriptionSlot,
bottomSlot, descriptionPlaceholder) through to DialogPromptSection. They
were declared on the public API but dropped on the floor before this
change, so Improve Kandev's contributor banner ("@octocat" /
"@kandev-maint") never rendered. Also gate the "or paste a GitHub URL"
toggle on lockedFields.repository so the locked-repo dialog hides it
(matches main's behaviour and the existing test expectation).
2. Revert "commits section starts collapsed" (introduced in
|
||
|
|
75f1732421 |
feat: add GitHub token injection for remote executors and Docker session resume (#654)
* feat: add GitHub token injection for remote executors and Docker session resume - Add global GitHub access token configuration (backend + frontend) - Inject GitHub token into Docker and Sprites executors via env vars and Git credential helper - Transition Docker executor from host mounting to repository cloning - Implement Docker session resume (reconnect to existing containers after backend restart) - Add shell validation using exec.LookPath in agentctl - Add scriptengine providers for agent install and agentctl placeholders * fix: format github-status.tsx with prettier * fix: resolve lint issues (goconst, errcheck, file-length) * fix: address code review findings - Fix resolveGHCLIToken to check both GITHUB_TOKEN and GH_TOKEN guards - Fix credential helper to use \\ fallback - Change /bin/bash to /bin/sh in credential helper for Alpine compat - Strip embedded token from .git/config after clone in prepare scripts - Extract buildLaunchAgentRequest helpers to stay under 80-line limit - Add tests for GitHubAuthProvider, injectTokenIntoURL, reconnect validation - Add GH_TOKEN-only test case for resolveGHCLIToken * refactor(executor): extract GitHub credential resolution to separate file Move injectGitHubToken, injectGitHubTokenFromCLI, resolveRemoteCredentials, resolveAuthSecrets, methodIDToEnvVar, resolveGHCLIToken, and detectGHToken out of executor_execute.go (was 803 logical lines, over the 800-line limit after merging main) into a new executor_credentials.go file. Also adds the nil-guard on secretStore in resolveAuthSecrets that was flagged by review. 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> |
||
|
|
c092fdeb1c | fix: ui polish — unified topbar, selector consistency, quick actions editor improvements (#693) | ||
|
|
ccc62d9324 |
feat(github): configurable quick-action presets and PR branch checkout (#689)
* feat(github): configurable quick-action presets and PR branch checkout - Add workspace-scoped action presets (icon, label, hint, prompt template) for PRs and Issues, editable from GitHub settings with reset-to-defaults. - When starting a task from a PR, check out the PR head branch directly in the worktree instead of branching off main. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(github): extract action-preset service to keep service.go under line limit Splits the preset CRUD methods out of the oversized service.go and applies prettier formatting to three frontend files flagged by CI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(github): address review feedback on action presets - Prevent infinite refetch loop when fetching presets fails by tracking attempted workspaces in a ref instead of re-entering the effect on every loading-state change. - Drop unused savePR/saveIssue helpers from the hook; only save() is consumed. - Reorder the init spread in updateGitHubActionPresets/resetGitHubActionPresets so callers can't accidentally overwrite method/body. - Tighten the GitHubActionPreset.icon type with string & {} so known icon keys keep autocomplete. - Sharpen the eye-icon test to assert the exact IconEye mapping. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(github): address second round of review feedback - Seed preset editor drafts from current store state via lazy initializers so a warm mount shows the user's saved customisations instead of silently reverting to defaults when Save is clicked. - Guard Reset button with its own in-flight state to prevent double-firing. - Stop freezing resolved defaults into the stored row on a partial update; persist only what the store has for the untouched kind so future default improvements still reach those workspaces. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(github): tighten quick-actions editor UI Replace the stacked PR/Issue editors and multi-line preset cards with a tabbed layout and single-line rows. Each row shows icon · label · hint inline; an "Edit prompt" toggle reveals the template textarea only when needed. Drops the redundant section headings, up/down reorder buttons, and verbose template helper label (hint moved into the textarea placeholder). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(github): widen icon select trigger so the icon is visible The icon select trigger had a fixed 40px width which the SelectTrigger's built-in chevron, padding, and icon gap all competed for, clipping the icon out of view. Drop the width override so the trigger sizes to its content. 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> |
||
|
|
85aa3ac0aa |
feat(github): add /github page for PRs and issues (#687)
* feat(github): add /github page for PRs and issues Dedicated page under /github lists pull requests and issues across the authenticated user's repos with preset filters, custom GitHub search queries, per-repo narrowing, saved queries, and one-click task launching for any PR or issue. Performance: - Cache user PR/issue search responses for 30s with singleflight to coalesce concurrent misses, so preset switching and pagination avoid upstream round trips. - Replace the per-row PR status fetch (N+1) with a batch endpoint POST /api/v1/github/prs/statuses that fans out concurrently on the backend with a semaphore and caches per-PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: apply Prettier formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address review feedback on PR cache + status batching - Harden searchCacheKey against separator collisions from user-controllable customQuery (length-prefix encoding). - Drop repositories.loaded hydration when inner workspace fetch fails so the client refetches instead of trusting a partial snapshot. - Guard useGitHubSearch with a monotonic request counter to drop stale responses when filter+page changes overlap. - Fix usePRStatuses teardown: depend only on composed key so a new array identity per render doesn't cancel the in-flight batch. - Correct godocs (duplicate, outdated state:open claim). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address additional review feedback - Restore state:open for issue watcher: buildIssueSearchQuery no longer injects it as a default, so existing watches were starting to surface closed issues. Inject state:open in buildIssueFilter so only the watcher path is affected; the /github page presets already set their own state qualifiers. - countCheckResults now excludes in-progress checks from the denominator so a `1/3` badge reflects pass rate on completed checks, not a mix of running and failed. The pending state is still carried via ChecksState. - usePRStatuses retries after a transient fetch error (only mark the key as completed once the fetch resolves). This also fixes missing badges under React Strict Mode's intentional double-mount, since the first effect's cleanup fires before the response lands and the guard stays unset for the second mount to retry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(github): add tests for my-github hooks Covers use-committed-query, buildParams from use-github-search, use-saved-presets readStorage validation, use-known-repos recordForKey accumulator semantics, and use-pr-statuses batch fetch lifecycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address review feedback on search cache and repo accumulator - Clamp perPage before composing the search cache key so identical pages (e.g. perPage=150 and =100) share a cache entry instead of creating duplicates keyed on the raw value. - Add resetKnownReposStore and call it on /github unmount so the module-level repo accumulator doesn't carry across navigations. - Rename use-pr-statuses test to match its assertion (it verifies stale statuses are cleared when the key changes and fetch fails). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: apply Prettier formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web): distinguish blocked from dirty in PR mergeable badge GitHub's blocked state (required reviews/checks/branch protection) is not a merge conflict, so don't label it "Conflicts". Surface it as "Blocked" with the pending tone; keep "Conflicts" for dirty. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address remaining /github code review comments - Route PR status + batch through handleSearchError so ErrNoClient surfaces as 503 instead of 500 (matches the search endpoints). - Sort workflow steps by position in the quick-task-launcher default pick, matching the convention in issue-watch-dialog. - Make PresetItem keyboard-accessible: role=button, tabIndex, and Enter/Space handling on the wrapping div (it contains a nested delete button, so it can't itself be a button). - Extract useRepoOptions out of useGitHubPageState to bring the hook back under the 100-line complexity limit. - Honor ctx.Done() when acquiring the PR-status batch semaphore so a cancelled HTTP request releases queued goroutines instead of running each upstream fetch to completion. - Clarify parsePaginationQuery doc: the upper bound (100) is enforced by clampSearchPage in the client layer, not here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): address /github review follow-ups - Merge duplicate @/lib/utils imports in pr-list. - Use rel="noopener noreferrer" on external PR/issue links (codebase convention) and add cursor-pointer per the UI guidelines. - Rewrite onOpenSaveDialog as an explicit if-block so the callback returns void rather than boolean | void. - Use PaginationEllipsis in its own PaginationItem so the ellipsis and the adjacent page link are separate list items instead of visually fused into a single cell. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(web): derive QuickTaskLauncher dialog state from props Replaces the useEffect+setState anti-pattern with useMemo-derived state driven directly by the payload prop. Drops the eslint suppression that was needed for react-hooks/set-state-in-effect. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
dc827a25d6 | fix(github): treat skipped checks as passing and add ready-to-merge status (#616) | ||
|
|
c56dfe8537 | feat(github): move PR monitoring to backend with lightweight polling (#502) | ||
|
|
123b2f6eed |
feat: start task from GitHub URL (#365)
* feat(backend): accept github_url in task creation and clone on launch Add github_url field to task creation DTO and propagate through HTTP/WS handlers and service layer. When a task is created with a GitHub URL instead of a local repository, the service resolves (or creates) the repository record via FindOrCreateRepository and triggers a clone/fetch via repoclone.Cloner during executor pre-flight. Wire RepoCloner into the orchestrator executor so remote repositories are fetched before the agent process starts. On resume, RepoUpdater performs a fetch to ensure the worktree is up-to-date. * feat(backend): add GitHub branches endpoint for remote repos Add ListRepoBranches to the GitHub client interface (GHClient, PATClient, NoopClient, MockClient) and expose it via GET /api/v1/github/repos/:owner/:repo/branches. The mock controller gains POST /api/v1/github/mock/branches for seeding branch data in E2E tests. * feat(web): add GitHub URL input to task create dialog Add a toggle in the task create dialog that switches between the existing repository selector and a free-text GitHub URL input. When a valid GitHub URL is entered (owner/repo parsed), branches are fetched from the new backend endpoint and auto-selected. The dialog state, effects, validation, and submit logic are extended to support the github_url flow alongside the existing repository flow. * test(e2e): add tests for task creation from GitHub URL Add E2E tests verifying: - Full flow: toggle to GitHub URL mode, enter URL, create task, verify agent runs and completes in session view - Toggle behavior: switching between GitHub URL and repository selector modes updates UI correctly * fix(web): dynamic URL input sizing and extract RepoSourceInput Replace fixed-width GitHub URL input with size-based auto-sizing (matching InlineTaskName pattern). Extract RepoSourceInput component from DialogHeaderContent to stay within cyclomatic complexity limits. Add testId prop to Combobox and wire it to ExecutorProfileSelector for E2E test targeting. * test(e2e): add worktree executor test and fix GitHub URL repo seeding Update GitHub URL E2E tests to pre-seed a repository with GitHub provider info pointing to the local git repo, avoiding actual clone attempts during task creation. Add test for GitHub URL task creation with worktree executor type, verifying the full flow including executor profile selection. Extend ApiClient with listExecutors and provider fields on createRepository for test seeding. * fix(backend): use protocol-aware clone URL for provider-backed repos The ensureRepoCloned function was hardcoding HTTPS clone URLs via repositoryCloneURL(), but the user's git setup may require SSH. Add BuildCloneURL to the RepoCloner interface so the executor uses the Cloner's configured protocol (SSH/HTTPS) instead of always generating HTTPS URLs. Falls back to HTTPS if BuildCloneURL fails. This fixes workspace loading issues (terminal, file tree) for cloned repos when the user has SSH-only authentication configured. * test(e2e): enhance GitHub URL test with terminal and changes panel verification * feat(web): add inline validation for GitHub URL input - Show inline error for invalid URL format (not matching github.com/owner/repo) - Show inline error when repository is not found or not accessible - Mock client returns error for unknown repos (matches real GitHub API) - E2E tests for invalid URL, nonexistent repo, and error-clearing on valid input * fix(web): fix GitHub URL branch auto-select and E2E test stability - Pick branch by name (main/master) for GitHub URL mode instead of using selectPreferredBranch which returns origin-prefixed names - Add test timeout for worktree executor test - Use shorter filename in terminal test to avoid xterm line-wrap issues * fix(web): fix GitHub URL dialog polish and wrong-repo bug UI fixes: - Align toggle button text with dropdown text content (pl-3) - Match GitHub URL input height to dropdown trigger (min-h-9) - Position error text absolutely to prevent layout shift - Remove redundant toast on repo-not-found (inline error suffices) Bug fix: - Prevent repo auto-select from overriding GitHub URL mode by guarding useRepositoryAutoSelectEffect with useGitHubUrl - Reorder getRepositoriesPayload to check useGitHubUrl first, preventing stale repositoryId from winning over the GitHub URL Add E2E test verifying correct repo usage across consecutive tasks. * fix(web): fix dialog height mismatch and error text overlap - Use exact h-9 on GitHub URL input to match dropdown trigger height - Move error text from absolute-positioned overlay to inline next to the toggle button, preventing overlap while keeping layout stable * fix(web): make GitHub URL input auto-size to content Reduce minimum size from 30 to 20 and shorten placeholder so the "/ task-name" sits closer to the URL text, matching the dropdown's compact w-auto behavior. * fix(web): polish GitHub URL input sizing, floating error, and placeholder color - Match input height (h-7) and padding (px-2) to Combobox trigger button - Size input width to exactly fit placeholder text - Show validation error as floating popover below input instead of inline - Keep placeholder text grey even when input has error state - Extract useGitHubUrlState hook to fix max-lines-per-function lint * fix(backend): return proper HTTP status codes from GitHub branches endpoint Add GitHubAPIError type to distinguish upstream GitHub API status codes. The branches endpoint now returns 404 (not 500) for non-existent repos, which the logging middleware correctly logs as DEBUG instead of ERROR. * feat(web): show error toast when a task session fails to start Add session failure notification plumbing: WS handler detects FAILED state with error_message, pushes to Zustand store, and a global bridge component shows an error toast via the ToastProvider. Also adds data-testid attributes to toast elements and types error_message on the session state changed payload. * style: fix prettier formatting in task-create-dialog and related files * fix(web): extract ToastList component to fix max-lines-per-function lint * fix: address code review feedback for GitHub URL task creation - Preserve 403 status instead of collapsing to 401 in branches endpoint - Use clone-generic error instead of remote-docker-specific in ensureRepoCloned - Trim whitespace in GitHub URL validation (backend + frontend) - Add GitHubURL field to wsUpdateTask path - Fix DefaultBranch mapping to prefer repoInput.DefaultBranch over BaseBranch - Clear githubBranchesLoading on early-return paths to prevent stuck state - Gate GitHub branch loading by mode to avoid false loading states - Encode owner/repo in API path for special characters - Add aria-label, aria-invalid, aria-errormessage for accessibility * fix(e2e): improve diff-update test stability with fresh locator and longer timeout * fix(backend): add --paginate flag to gh api branches call for repos with >100 branches * fix: address second round of code review feedback - Add pagination support to PATClient.ListRepoBranches for repos with >100 branches - Validate owner/repo in mock controller addBranches endpoint - Use aria-describedby instead of aria-errormessage for broader AT support - Add aria-live region to toast container for screen reader announcements - Assert absence of FIRST_MODIFICATION in diff-update E2E test * fix(backend): downgrade HTTP request logger from error to warn for 4xx/5xx responses * fix: address third round of code review feedback - Validate repositoryID/localPath in UpdateRepositoryLocalPath - Copy slices in mock client to prevent external mutation - Extract GitHub header constants and shared setter in PATClient - Redact clone URL from logs to prevent credential leaks - Guard against persisting empty localPath after clone - Show fallback toast message when FAILED state has no error_message |
||
|
|
8b5681bee2 | feat: startup health status (#344) | ||
|
|
2782d826ec | feat: improve PR review watcher + PR info panel (#281) | ||
|
|
32f7ceb319 | feat: several UX improvements (#272) | ||
|
|
43eea81dfe | feat: pr watcher user or team review (#270) | ||
|
|
dff0b49c51 | docs: add github integration (pr watcher) (#262) |