* feat: sync workflows from a configured github repo
Per-workspace setting pointing at a GitHub repo directory of portable
workflow export files. A background poller fetches it on an interval and
creates, updates, or deletes the matching workflows; a force-sync endpoint
bypasses the content-hash check. Synced workflows carry source/source_path
provenance and coexist with manual ones; changes that cannot be applied
safely (steps or workflows that still hold tasks, ambiguous step names,
unparseable files) surface as warnings in the new settings section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): fill workflow sync form from a pasted github link
A new Repository link field parses plain repo URLs, SSH remotes, and
tree/blob links (which also carry branch and directory) into the
structured owner/repo/branch/path fields.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): simplify workflow sync form to link, branch, and interval
The repository link is now the only repo input: owner, repo, and
directory resolve from the pasted URL (echoed as a summary under the
field), and a stored config renders back into a canonical tree link via
buildGitHubRepoUrl. Branch and poll interval stay individually editable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): collapse workflow sync into a button, dialog, and status card
The Workflows settings page now shows only a GitHub Sync button by
default; the link/branch/interval form lives in a dialog that closes on
successful save or removal. Once configured, a compact status card
(mirroring the GitHub connection-status card) shows the synced repo,
branch, directory, last result, warnings, and a Sync now action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): move github sync button into the workflows page header
The GitHub Sync trigger now stacks under the Workspace settings button
on the top right, and the extra separator around the sync area is gone —
unconfigured workspaces show no sync chrome at all, configured ones show
just the status card above the workflow list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): move github sync button next to the workflow actions
GitHub Sync now sits in the Workflows section toolbar alongside
Export All, Import, and Add Workflow; the status card renders inside the
section above the workflow list. The toolbar moved to its own component
to keep the page under the file-size limit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): separate sync status card from the workflow list
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): align sync status card with the workflow list gutter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: repair local edits to synced workflows on periodic sync
Periodic syncs previously skipped applying when the repo content hash
was unchanged, so local UI edits to a synced workflow were only reverted
by a manual Sync now. Every sync now reconciles against the repo; the
applier compares each step against its definition and only writes (and
broadcasts) actual differences, so a no-drift sync stays silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: make github-synced workflows read-only
Workflows with source=github reject UI mutations: workflow rename,
description, agent-profile, and delete return 409 from the task-domain
handlers; step create/update/delete/reorder and template application are
guarded in the workflow controller. The sync applier writes through the
service/repo layer and stays exempt. The frontend disables the card's
inputs, save/delete, and the step pipeline editor for synced workflows,
with a tooltip pointing at the synced repository. Drift repair on sync
remains as the backstop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: optional workflow sync polling and repo-derived branch picker
A per-workspace auto-sync toggle (poll_enabled, default on) controls
the background polling loop; when off the workspace only syncs via Sync
now. The dialog's branch field becomes a select fed by the repository's
actual branches (same loader the task-create dialog uses), falling back
to free text until branches load. Existing dev databases gain the new
column via an idempotent ALTER.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: release synced workflows when the sync config is removed
Deleting a workspace's workflow sync configuration converts its
GitHub-sourced workflows back to manual ownership (source=manual), so
they become editable instead of being stranded read-only. Release runs
before the config row is deleted so a failure keeps the config for
retry. The settings page reloads after removal (and after syncs that
changed workflows) so cards unlock without a manual refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): drop branch selector and compact the auto-sync row
The pasted link already carries the branch (main by default), so the
dialog no longer needs a branch field — the resolved summary echoes it.
The auto-sync toggle is now one compact row with the interval inline and
a single short helper line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: address PR review feedback
Backend: fail-open read-only guard on workflow lookup errors (fixes MCP
step handler CI failures); poller Stop holds its mutex through drain;
per-workspace lock serializes syncs against config delete/replace;
rollback workflow creation when provenance stamping fails; idiomatic
duplicate-column migration via db.IsDuplicateColumnError; branch-name
validation and a 30-day interval cap; generic 500s with logged detail;
WS read-only rejections use CONFLICT with the real reason; e2e reset
aborts when sync-config cleanup fails; test hygiene (t.Cleanup,
SetMaxOpenConns).
Frontend: malformed percent escapes parse as invalid instead of
throwing; save disabled during initial load and for invalid intervals;
Remove disabled while saving; stale async config responses discarded;
delete-dialog confirms re-check read-only; focusable Synced badge;
explicit button types; failed polls labeled 'last attempt'; unique
warning list keys; test cleanup via afterEach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: guard workflow sync test fake against concurrent poller access
The poller invokes the fake applier from its own goroutine while the
synctest test asserts on call counts; -race caught the unguarded reads.
Also documents that the per-workspace lock is deliberately held across
the GitHub fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: coalesce ci auto-fix queue rounds
* fix: centralize ci auto-fix round cap
* fix: keep ci auto-fix state fresh
* fix: refine ci auto-fix round status
* fix: handle ci auto-fix exhaustion edge cases
* fix: reduce ci auto-fix dispatch complexity
* fix: refresh pending ci auto-fix before direct prompt
* fix: harden ci auto-fix queue replacement
---------
Co-authored-by: root <root@kandev.ts.net>
* fix(github): hide approve PR button on own PRs and reject self-approval server-side
The frontend's existing self-author check failed open when the current
GitHub user hadn't loaded yet — the PR panel didn't call useGitHubStatus,
so the green Approve button flashed on every PR (including your own)
during the brief window before /api/v1/github/status resolved.
Fail closed when the user is unknown, fetch the status from the panel,
and add a typed ErrSelfApprove guard in Service.SubmitReview so any path
that bypasses the visibility check gets a clean 422 instead of an opaque
upstream error.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(github): cover controller's self-approve 422 mapping
Address PR review feedback: add TestHttpSubmitReview_SelfApproveReturns422
to exercise the controller → service → 422 path end-to-end so a future
refactor can't silently reclassify ErrSelfApprove as a generic 500.
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(share): add public share links via GitHub Gists
Adds a Share button to the chat panel header that publishes the
current session as a secret GitHub Gist on the user's account. The
share link routes through gistpreview.github.io to render share.html
as a styled kandev-themed page; snapshot.json + README.md ride along
for machine-readable + fallback views.
Snapshots run a redaction pass (absolute paths, common credential
shapes, .env contents, shell env vars) and strip <kandev-system>
prompt blocks via the shared sysprompt helper. Message filtering
whitelists user prose + tool_* types so internal noise never leaks
into shares.
Spec at docs/specs/public-share-links/spec.md.
* fix(share): coerce pendingClarification to bool for ClarificationSection
A late refactor extracted ClarificationSection with `pendingClarification: boolean`
on its props, but the call site passes the actual Message|null value. CI's tsc
build catches the mismatch; runtime was unaffected because the section only uses
the value in a truthiness check. Coerce with Boolean() at the call site to keep
the prop's intent narrow.
* fix(share): address PR #995 review feedback
Bugs (flagged by Greptile, Cubic, Claude):
- abs-path redaction split on `root + "/"` so sibling dirs like
/home/user/proj2 don't get corrupted when workspaceRoot is
/home/user/proj; regression test added.
- GHClient.DeleteGist now wraps "HTTP 404" stderr as *GitHubAPIError so
share.IsAlreadyGone matches consistently with the PAT client; this
restores the "gist already revoked upstream" soft-success path for
CLI-authenticated users.
- SnapshotSizeBytes is now measured from the same MarshalIndent payload
the gist backend uploads, so the size shown in the UI matches the
actual gist bytes (was ~20-40% underreported).
Polish (flagged by CodeRabbit, Cubic):
- Tool name + tool output now HTML-escaped; tool call args and output
render via <pre><code> instead of triple-backtick fences so payloads
containing ``` can't break out of the code block.
- share-list.tsx: clear `busy` via finally and break error layout out
of the justify-between row.
- use-shares.ts: convert UseSharesResult to interface (repo TS rules);
reset error+isLoading when ids are missing.
- use-shares.test.tsx: wait on the actual async outcome (shares
populated, listShares called) instead of isLoading flipping false.
- provider.go: replace `repo.Close` cleanup with an explicit noop
closure so the comment ("Cleanup is a no-op") and the impl match.
- spec.md: aligned the shareability rule across What/Failure/Scenarios/
Out-of-scope — any non-prehistory state can be shared; added language
tags to fenced code blocks for markdownlint.
Skipped intentionally (reply on each thread):
- HTML-escape every metadata badge: the values are controlled enum
strings from kandev's own schema, not user input. Defense-in-depth
noise. Tool name (which can come from agent runtime) IS escaped.
- useShares request-token guard against stale responses: the dialog
owns the hook lifecycle and opens against a single (taskId,
sessionId) — the race the comment describes can't occur here.
* fix(share): redact paths for every worktree, not just the first
A single TaskSession can carry multiple worktrees when the task spans
multiple repos. The redactor previously only saw the first worktree
path, so absolute paths from secondary worktrees leaked into public
shares instead of being rewritten to repo-relative form.
NewRedactor is now variadic and applies the abs-path rule against every
root it receives, sorted longest-first so a root that is a prefix of
another (e.g. /workspace vs /workspace/repo1) gets matched by the more
specific one first. workspaceRootFor in builder.go becomes
workspaceRootsFor and emits every WorktreePath plus the WorkspacePath
fallback.
Flagged by Cubic on PR #995.
* fix(share): address PR #995 review round 3
Bugs (flagged by Cubic + Claude on gh_client.go:714):
- DeleteGist now calls the existing isNotFoundErr helper instead of an
inline strings.Contains(err, "HTTP 404"). The helper also covers
"404 Not Found" and "status: 404", which some gh versions emit, so
the share service's "gist already revoked upstream" soft-success
path now fires consistently across CLI auth users.
Bugs (flagged by Claude on builder.go:220):
- toolCallBlock used to read metadata["tool_name"] and metadata["args"]
but production never writes those keys; the typed payload lives under
metadata["normalized"] (a *streams.NormalizedPayload) and the tool
kind is on Message.Type. Switched to deriving ToolName from
strings.TrimPrefix(m.Type, "tool_") (gives "read", "execute", "edit",
…) and reading args from metadata["normalized"], with backward-compat
fallback to the older "tool_name"/"args" keys for the debug-fixture
replay path. New TestBuildSnapshot_ToolCall_ProductionMetadataShape
exercises the real session shape.
Bugs (flagged by CodeRabbit on redaction.go:110):
- Bare workspace-root occurrences (pwd output, end-of-sentence, quoted
paths) used to leak because the matcher only handled root + "/".
Switched to a precompiled regex per root —
regexp.QuoteMeta(root) + `(?:/|\b)` — so both `root/...` and standalone
`root` get stripped, while sibling dirs like /workspace2 still don't
match. Test TestRedactor_AbsPath_StandaloneRootStripped covers the
bare-root variants.
Doc (flagged by Claude on spec.md):
- Two malformed fence closers (`go and `text on closing fences) corrected
to plain ``` so the file is markdownlint-clean.
* fix(share): narrow abs-path boundary to avoid sibling-prefix corruption
Round 4 review (Claude + CodeRabbit) caught a regression in the regex
introduced by 436cb66: `(?:/|\b)` used `\b` (word boundary) to catch
bare-root cases, but `\b` fires on any word↔non-word transition. So a
sibling path like `/workspace.bak`, `/workspace-old`, or
`/workspace-other` would false-match the `/workspace` root (since
e→. and e→- are word boundaries), stripping it and leaving a
corrupted `.bak` / `-old/file.ts` in the share.
Replaced the matcher with `(/|\s|$)`: path separator, whitespace, or
end-of-string only — never punctuation or quote chars. The boundary
is captured and re-emitted via ReplaceAllStringFunc (or dropped, in
the `/` case) so prose loses only the root, not the surrounding space.
Added TestRedactor_AbsPath_SiblingsWithPunctuationNotCorrupted covering
`.bak`, `-old`, `_alt` siblings; the digit-sibling test still passes.
The quoted-bare-root case (`"/workspace"`) no longer matches and was
dropped from the standalone-root test — contrived enough that a small
leak there is preferable to corrupting realistic sibling paths.
* refactor(share): move share button into chat status bar
Replaces the dedicated PanelHeaderBarSplit above the message list with
a slot in ChatStatusBar (the existing row holding TodoIndicator and
PRStatusChip just above the chat input). The button anchors right via
ml-auto; when the "proceed to next step" affordance is present it
takes the right anchor instead and the share button stays inline.
Also tightens the iconOnly variant in ShareButton to a ghost style
that matches the muted-foreground chips in the status bar, and exports
shareableSessionStateClient from share-button.tsx so the gating logic
lives next to the entry point it controls.
Net -23 lines in task-chat-panel.tsx: the extra header bar and the
local gate helper are gone.
* docs(share): broaden tooltip + add share/ to CLAUDE.md tree
The share tooltip read "Share this completed task with a public link",
but shareableSessionStateClient allows RUNNING / IDLE / WAITING_FOR_INPUT
/ FAILED / CANCELLED too. A user hovering mid-run saw misleading copy;
reworded to "Share this task conversation as a public link" which
matches the actual gate.
Also added the missing internal/task/share/ entry to the task/ subtree
in CLAUDE.md so the new package shows up in the repo map — flagged
across multiple rounds.
* docs: add internal/task/share/ to repo map
Carryover from the share PR — AGENTS.md (which CLAUDE.md symlinks to)
was missing the share/ subpackage under task/. Adding it now so the
repo map matches what's checked in.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* 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>
* feat(github): add merge button when PR is ready to merge
Surfaces a one-click merge action in the PR detail panel header and as a
compact green pill in the PR topbar hover popover, gated by the existing
isPRReadyToMerge predicate (open, CI success, mergeable_state=clean, and
either approved or no review process).
Backend adds MergePR to the Client interface, implements it across
PAT/gh/Noop/Mock clients, exposes PUT /api/v1/github/prs/:owner/:repo/:number/merge,
and maps GitHub 405/409 to HTTP 409 so the UI can show a clear error.
* fix(github): address PR review feedback on merge endpoint
- Map gh CLI 404/403/405/409 stderr to *GitHubAPIError via a new
ghMergeStatusCode helper, so merge conflicts surface as HTTP 409 for
gh CLI users (previously only PAT users got the right status).
- Service.MergePR returns ErrNoClient when unconfigured; httpMergePR
translates that to HTTP 503 with github_not_configured, matching
httpListRepoBranches.
- httpMergePR distinguishes empty body (io.EOF, fall through to default
merge method) from malformed JSON (HTTP 400 invalid payload).
- PRMergeButton tracks a local merged flag so the button stays hidden
across the async refresh window after a successful merge.
* test(github): cover merge helpers and extract PRMergeButton
- TestGhMergeStatusCode locks in the gh CLI stderr → status-code parser
for nil, unrelated errors, the HTTP / status: / bare-phrase forms of
404/403/405/409, and a 500 non-match guard.
- Broaden ghMergeStatusCode to also match "405 Method Not Allowed" /
"409 Conflict" phrase forms so the merge endpoint's 409 mapping does
not depend on the gh CLI keeping its current "HTTP <code>" prefix.
- TestHttpMergePR_MalformedJSON covers the new non-EOF bind-error branch
(truncated body → 400). TestHttpMergePR_NoClient_Returns503 covers the
ErrNoClient → 503 path on Service.MergePR.
- Document the post/put error-type divergence on PATClient.post so a
future caller does not silently lose per-status mapping.
- Move PRMergeButton out of pr-shared.tsx into its own file, and reset
the local "merged" flag when taskPR.id changes so an instance that
re-renders for a different PR does not stay hidden.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* 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(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>