* refactor: move integrations to top-level settings with install-wide configs
Jira and Linear configurations are now install-wide singletons rather than
per-workspace duplicates, and the GitHub/Jira/Linear settings pages live
under /settings/integrations instead of being nested inside each workspace.
Issue and PR watchers stay per-workspace via a workspace switcher inside
each integration page. Backend ships one-shot migrations that promote the
most-recently-updated row and copy its secret onto the new global key.
* fix: address review feedback on integrations refactor
Fix migration crash on installs whose jira_configs predate the auth-health
columns by selecting only guaranteed-present legacy columns and falling back
to defaults for the rest. Cover the empty-legacy-table and pre-health-columns
upgrade paths with new store tests.
On the frontend, refactor useIntegrationEnabled to take string parameters
instead of an inline options object so getSnapshot stays referentially stable
across renders, and stop dispatching a misleading no-op sync event when
localStorage.setItem fails in private mode.
* fix: hydrate workspace list on integration settings pages
The integration pages wrapped their content in a fresh StateProvider, which
created a nested store scope with an empty initial state and shadowed the
settings layout's hydrated workspace list. WorkspaceScopedSection then saw
zero workspaces and rendered the "Create a workspace to configure this
integration" empty state even when the user had workspaces.
Drop the redundant StateProvider from the jira and linear pages so they share
the layout's store. The github page switches to StateHydrator so it can still
seed the GitHub OAuth status without resetting the rest of the store.
* refactor(integrations): flatten watcher tables, pick workspace in create dialog
Hiding watchers behind a workspace switcher made the list invisible at a
glance — a user with multiple workspaces had to flip between each one to
see what was configured. Replace the switcher with a flat table that lists
every watch across every workspace, with a Workspace column for context, and
move workspace selection into the create dialog so the user picks once when
the watcher is born.
Backend: jira list-issue-watches and github list-review-watches /
list-issue-watches accept the workspace_id query param as optional; when
omitted, the corresponding store ListAll* method returns rows across every
workspace. Per-row mutations still require workspace_id as the IDOR guard.
Frontend: useReviewWatches / useIssueWatches / useJiraIssueWatches gain a
three-state contract — string for one workspace, undefined for all
workspaces, null to skip fetching. Watch tables grow an optional
showWorkspace column. Watch dialogs accept an optional workspaceId prop;
when absent, the dialog renders a workspace dropdown for the create flow
(disabled when editing existing rows so refs aren't orphaned). Github
review-watch-dialog crossed the 600-line lint cap so the prompt editor
moved to review-watch-prompt-field.tsx.
* fix(integrations): make watcher step dropdown reactive across workspace switches
Switching to a workspace whose workflows had to be fetched left the workflow
step dropdown stuck — the user could pick a workflow but the step Select
either showed the previous workflow's steps or appeared inert. The cause was
the per-dialog useWorkflowSteps hook deferring its empty-state reset via
Promise.resolve().then(() => setSteps([])) to dodge a setState-in-effect
lint rule, which let stale steps survive into the next render and gave Radix
nothing to anchor the popover to during the in-flight fetch.
Hoist the hook into a shared useWorkflowSteps that resets via React's
official setState-during-render pattern, exposes a loading flag, and returns
a stepPlaceholder helper so the Select can render "Loading steps…" or "No
steps in this workflow" instead of an empty popover. The Step Select is now
also disabled while loading or when there are no steps, making the state of
the dropdown unambiguous.
* fix: address PR review feedback on integrations refactor
Backend:
- linear migration: add column-presence guard mirroring the Jira fix so
installs that upgraded from the original Linear schema (without org_slug
/ last_checked_at / last_ok / last_error) don't crash startup with
"no such column"; add TestStore_MigrateLegacyPerWorkspaceTable_PreHealthColumns
- jira/handlers.go: drop the now-unused errMsgWorkspaceIDRequired constant
that was failing golangci-lint
- jira/mock_controller_test.go (new): cover setAuthHealth happy-path and
invalid-JSON-rejection so the singleton-shaped payload is locked in
Frontend:
- useIntegrationAvailable: gate the auth-health probe on loaded && enabled
so disabled integrations don't poll /config every 90s
- use-workflow-steps: initialize loading to !!workflowId so edit-mode opens
show "Loading steps…" instead of "No steps in this workflow" before the
first fetch lands
- use-workflow-steps.test.ts (new): cover the empty/non-empty workflowId,
workflow switch, fetch error, and stepPlaceholder branches
- use-linear-enabled.test: tighten the legacy-key migration assertion to
pin the migrated value rather than just verifying key cleanup
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat: add Linear integration
Mirrors the existing Jira integration so a workspace can connect to Linear
with a personal API key, browse and search issues, and start a Kandev task
from any issue. Backend uses Linear's GraphQL API with a workspace-scoped
config + secret store and a 90s auth-health poller; the frontend adds a
settings page, an issue dialog with state transitions, link/import buttons
in the task UI, and a paginated /linear browse page with a Start task flow.
* fix(linear): address PR review feedback
- Route httpListStates through Service.ListStates instead of clientFor
- Extract repeated workspace_id required string into a constant
- Log GetConfig errors during credential resolution instead of dropping them
- Clear popover error state on close in import + link buttons
- Pick the first workflow with steps in the quick task launcher (don't hard-pin to workflows[0])
- Reset useLinearEnabled state on workspace change via tag-based pattern
- Add cursor-pointer to anchor + Link elements per project convention
- Add tests: handlers_test.go, linear-api.test.ts, use-linear-enabled.test.ts, linear-issue-common.test.tsx
- Document internal/jira and internal/linear in CLAUDE.md/AGENTS.md
* fix(linear): address second-round PR review feedback
- Round Linear's fractional state position instead of truncating it via
int(math.Round(s.Position)) so adjacent fractional positions don't collapse
- Force LinearSettings to remount on workspace change with key={id} so the
previous workspace's config / form / testResult / teams cannot bleed into
the next workspace before the new fetches settle
- Broaden the legacy-value useLinearEnabled test to cover "true", "1", "yes",
and "legacy" so a regression to strict value === "true" would fail
* fix(linear): address third-round PR review feedback
- Propagate context.Canceled / context.DeadlineExceeded from ListStates in
GetIssue instead of swallowing them — a caller cancellation must not return
a partial-success response
- Drop the one-off contains() test helper in handlers_test.go and use
strings.Contains directly so the assertion is self-documenting
- Add getLinearIssue URL-shape test that locks identifier-as-path-segment
vs workspaceId-as-query-param so accidentally swapping them would fail
* test(linear): extract repeated literals in linear-api.test.ts
Pulls "ws-1", the API base URL, and the auth-method literal into module-
level constants so the assertions read as the route shape they care about
and the duplicate-strings ESLint rule (currently warn, threshold 4)
stays quiet as the file grows.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(web): hide Jira buttons when disabled or auth failing
Hide the Jira import button on the task creation dialog and the "Link Jira" button on the task top bar when the workspace toggle is off, no Jira config exists, or the most recent backend auth probe failed. Centralizes the gating logic in a shared useJiraAvailable hook.
* fix(web): address PR review feedback on Jira availability gate
- Gate the auth probe on `loaded` from useJiraEnabled so it never fires when the toggle starts disabled
- Reset cached auth on workspace change so the previous workspace's `true` can't briefly leak into the new one
- JiraTopbarButton now consumes the shared useJiraAvailable hook instead of replicating the two-step gate inline
* fix(web): apply prettier formatting to use-jira-availability.ts
* fix(web): tag Jira auth state with workspaceId to avoid poll-tick flicker
Storing `{workspaceId, authed}` together and filtering at read time invalidates stale auth synchronously when the workspace switches, removing the need for a setAuthed(false) reset that was previously firing on every 90s poll tick. Adds a fake-timer test that asserts no false render slips through across a poll tick while auth stays healthy.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(jira): add Jira integration for ticket browsing, import, and task linking
Adds a workspace-scoped Atlassian Cloud integration: per-workspace settings
(site URL, email, API token or session cookie), encrypted secret storage,
and a REST client for tickets, projects, search, and transitions. A
background poller probes credentials on a 90s cadence and the keepalive
runs immediately on save so the UI's connected indicator updates without
delay.
The /jira page provides ticket browsing with filter pills, custom JQL,
saved views (including JQL), and configurable task-launch presets that
prefill TaskCreateDialog. JiraLinkButton on the task top bar links an
existing task to a ticket by prepending its key to the title; the ticket
popover surfaces details, transitions, and re-auth prompts on session
expiry. Topbar shows separate icon buttons for GitHub and Jira when each
is configured.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): address PR review feedback
- Token-based pagination (pageToken/isLast) replaces broken offset/total pagination
- Race-fix in service.clientFor: re-check cache after acquiring write lock
- sync.Mutex guards poller started/cancel state against concurrent Start/Stop
- Channel-based + atomic.Int32 test sync replaces time.Sleep polling in poller_test
- Async auth probe on SetConfig with detached context.Background goroutine
- Distinguish 4xx vs 5xx in httpSetConfig via ErrInvalidConfig sentinel
- Propagate 4xx APIErrors from ListTransitions in GetTicket
- Distinguish "not found" from real errors in jiraSecretAdapter.Exists
- Log secret Exists errors instead of swallowing
- walkADF: handle softBreak (was only hardBreak despite the doc-comment)
- Consolidate JIRA_KEY_RE in jira-ticket-common; remove duplicates
- ticket-row imports formatRelative/statusBadgeClass from common
- isFilterState type guard validates SavedView.filters shape
- task-presets readStorage preserves user's "cleared all" intent (returns [])
- task-presets-section gates draft sync on `loaded` to avoid wiping in-progress edits
- jira-api: caller `init` no longer overrides method/body via spread order
- jira-settings: disable Test button when no saved/typed secret
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): record auth probe failure even when probe ctx expires
- RecordAuthHealth now uses a fresh context.Background-derived ctx with a
5s timeout for the UpdateAuthHealth DB write, so a probe that consumes
its full 15s deadline still persists the failure (greptile P1).
- SetConfig goroutine drops its redundant outer timeout — RecordAuthHealth
manages its own probe timeout, so context.Background is enough.
- Add Service.SetProbeHook so tests can synchronize on probe completion;
service_test waitForAuthProbe now blocks on the hook channel instead of
time.Sleep polling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): address coderabbit review feedback (round 3)
- url.PathEscape ticketKey in GetTicket/ListTransitions/DoTransition paths
to harden against malicious keys ('../../foo', 'KEY?override=x').
- factoryHit on the test fixture is now sync/atomic.Int32; the async probe
goroutine spawned by SetConfig races the test thread under -race.
- GetConfig logs Reveal failures (was silently dropped, leaving the UI to
show "no expiry info" indistinguishably from a freshly-saved cookie).
- DeleteConfig logs secret-delete errors instead of swallowing them.
- clientFor distinguishes a secret-store failure from "not configured" so
the UI doesn't render the configure-CTA for transient backend errors.
- Unify ErrNotConfigured wire code: HTTP and WebSocket both return
"JIRA_NOT_CONFIGURED" via a shared constant.
- JiraLinkButton strips a leading "PROJ-123: " before re-prefixing so
re-linking a task replaces the prefix instead of stacking it.
- jira-settings clears testResult on save/delete so the alert reflects
only the currently-saved credentials.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(jira): cover Reveal error path in clientFor
Reveal-error case is distinct from "no config row" — the former wraps
the underlying error, the latter returns ErrNotConfigured. Without
this test the changed contract has no regression coverage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): route secret adapter Set through Exists
Treating every Get error as "not found" turned a transient DB error on
an existing row into a constraint-violation Create that masked the real
cause. Reuse Exists so the "secret not found:" prefix is the only path
that falls through to Create.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(jira): cover jiraSecretAdapter.Set branching
Routing Set through Exists changed the contract: a transient DB error
in Get now propagates instead of cascading into a constraint-violation
Create. Add three tests so a future refactor cannot regress this without
CI catching it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(jira): split Reveal errors and guard Save for new configs
- resolveCredentials now mirrors the clientFor split: a Reveal error
surfaces as `read jira secret: %w` with a warn log instead of
collapsing into "no token stored — paste one to test".
- disableSave gains the same missing-secret guard as disableTest so a
brand-new config can no longer POST an empty secret and leave the
workspace stuck with hasSecret=false.
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>
* 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>
* feat(ui): add responsive layouts to swimlane kanban content
Mobile gets swipeable columns with tab navigation and touch-friendly
drag sensors. Tablet gets 2-column horizontal scroll with snap.
Desktop keeps the existing CSS grid layout.
* feat(ui): add mobile task sheet and search bar components
Bottom sheet (vaul Drawer) for task preview with Edit/Delete/Open Session
actions. Compact search bar component for filtering tasks on mobile.
* feat(ui): wire mobile components into kanban board
Integrate MobileFab, MobileSearchBar, and MobileTaskSheet into the
kanban board. Skip desktop preview panel on mobile — card taps open
a bottom sheet instead of navigating directly.
* refactor(ui): optimize swimlane container for mobile
Hide swimlane headers for single workflow on mobile, remove horizontal
padding on small screens. Extract filterTasks and WorkflowItem to keep
the main component under the line limit.
* test: add mobile kanban E2E tests
8 Playwright tests covering mobile layout, FAB, search, column tabs,
bottom sheet, preview panel suppression, and swimlane header hiding.
Adds mobile-chrome project (Pixel 5) to playwright config.
* fix(ui): fix mobile E2E test failures in CI
Exclude mobile tests from chromium project (wrong viewport), add
startIndex to Embla carousel to fix initial scroll race condition,
fix strict locator for menu heading, use first step for swimlane test.
* fix(ui): fix create task dialog button layout on mobile
Make StartTaskSplitButton full-width on mobile, hide dropdown chevron
on small screens, and show a visible "Create only" button instead.
* fix(ui): match header button sizes and add icon to create-only action
Align hamburger menu button with changelog/health buttons by removing
explicit h-10 w-10 override. Add IconPlus to "Create without starting
agent" in both the desktop dropdown and mobile button.
* fix(ui): address code review feedback for mobile kanban
- Base hideHeaders on workflowFilter/total count, not filtered result
- Clamp mobile column index via useMemo when steps change
- Fix wrong assertion in preview panel test (was checking toBeVisible)
- Separate altDisabled from splitDisabled (no agent needed for create-only)
- Add visible plan mode button on mobile (was hidden behind dropdown)
- Wait for mobile-kanban-layout in page object goto()
- Extract computeBaseDisabled to reduce complexity
* fix(test): fix flaky mobile E2E tests in headless Chrome
- Use instant jump (scrollTo with jump=true) for tab-triggered carousel
navigation instead of animated scroll that doesn't complete in headless
- Add data-testid to swimlane header to avoid ambiguous Badge locator
matching column tab buttons
- Add timeout to viewport assertion for resilience
* fix(ui): address review feedback and fix carousel reinitialization
- Stabilize Embla options to prevent carousel reinit on tab change
(startIndex was causing re-initialization, resetting scroll position)
- Guard programmatic scrolls from triggering select callback revert
- Remove PointerSensor from mobile DnD sensors (conflicts with touch)
- Fix plan mode button using wrong disabled prop on mobile
- Use literal name matching in columnTab locator (avoid regex injection)
- Use toHaveCount(0) for stricter preview panel absence assertion
* fix(ui): use pointerDown gating for carousel swipe sync
Embla's scrollTo doesn't work in headless Chrome, causing the select
event to fire with index 0 and reset the active tab. Fix by only
syncing Embla→tabs on user-initiated pointer interactions (swipes),
not on programmatic scrollTo calls.
Update E2E test to verify tab active state instead of carousel
viewport positions, which are unreliable in headless environments.
* fix(test): add data-testid and data-active to column tabs
Use explicit data attributes for more reliable test targeting
instead of getByRole and CSS class assertions.
* refactor(ui): use touch sensors for tablet and deduplicate getTasksForStep
* fix(ui): improve spacing for stats and settings buttons in mobile menu