Files
kandev/apps/backend/internal/gitlab/watch_models.go
T
João Salavisa dfaab11959 feat: full GitLab integration — parity with GitHub (#1120)
* feat(backend): full GitLab parity — store, service, poller, WS, presets

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

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

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

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

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

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

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

Address Claude review blockers on the parity PR:

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

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

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

Address the second round of Claude review blockers:

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

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

Address remaining Claude blockers:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 22:10:07 +01:00

347 lines
16 KiB
Go

package gitlab
import "time"
// ProjectFilter identifies a GitLab project for review/issue watch filtering.
// Path is the namespace/path slug, e.g. "group/sub/project".
type ProjectFilter struct {
Path string `json:"path"`
}
// ReviewScope controls which reviewer_username scope is used for the review
// watch. GitLab does not have team-level review requests, so the scope set is
// smaller than GitHub's, but we keep the constant set parallel for symmetry.
const (
// ReviewScopeUser matches MRs where the user is explicitly the reviewer.
ReviewScopeUser = "user"
// ReviewScopeUserAndTeams is treated identically to ReviewScopeUser on
// GitLab (no team review requests) — kept for cross-integration symmetry.
ReviewScopeUserAndTeams = "user_and_teams"
)
// CleanupPolicy controls how a review or issue watch handles its auto-created
// tasks once the underlying MR / issue reaches a terminal state.
const (
// CleanupPolicyAuto deletes the task once the MR/issue is merged or closed
// UNLESS the user authored at least one message in the task (the agent's
// auto-start prompt does not count).
CleanupPolicyAuto = "auto"
// CleanupPolicyAlways deletes the task on terminal state regardless of
// user interaction.
CleanupPolicyAlways = "always"
// CleanupPolicyNever disables automatic cleanup.
CleanupPolicyNever = "never"
)
// IsValidCleanupPolicy reports whether s is one of the recognized policies.
// Empty string is treated as valid so legacy rows default to "auto".
func IsValidCleanupPolicy(s string) bool {
switch s {
case "", CleanupPolicyAuto, CleanupPolicyAlways, CleanupPolicyNever:
return true
}
return false
}
// NormalizeCleanupPolicy maps the empty string to CleanupPolicyAuto.
func NormalizeCleanupPolicy(s string) string {
if s == "" {
return CleanupPolicyAuto
}
return s
}
// MRWatch tracks active MR monitoring (session → MR). RepositoryID identifies
// which task repository the watched MR belongs to (multi-repo support; empty
// for single-repo tasks).
type MRWatch struct {
ID string `json:"id" db:"id"`
SessionID string `json:"session_id" db:"session_id"`
TaskID string `json:"task_id" db:"task_id"`
RepositoryID string `json:"repository_id,omitempty" db:"repository_id"`
ProjectPath string `json:"project_path" db:"project_path"`
MRIID int `json:"mr_iid" db:"mr_iid"`
Branch string `json:"branch" db:"branch"`
LastCheckedAt *time.Time `json:"last_checked_at,omitempty" db:"last_checked_at"`
LastNoteAt *time.Time `json:"last_note_at,omitempty" db:"last_note_at"`
LastPipelineState string `json:"last_pipeline_state" db:"last_pipeline_state"`
LastApprovalState string `json:"last_approval_state" db:"last_approval_state"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// ReviewWatch configures periodic polling for MRs needing the user's review.
// Projects holds the list of projects to monitor. An empty list means all
// projects the user has access to.
type ReviewWatch struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
WorkflowID string `json:"workflow_id" db:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"`
Projects []ProjectFilter `json:"projects" db:"-"`
ProjectsJSON string `json:"-" db:"projects"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"`
Prompt string `json:"prompt" db:"prompt"`
ReviewScope string `json:"review_scope" db:"review_scope"`
CustomQuery string `json:"custom_query" db:"custom_query"`
Enabled bool `json:"enabled" db:"enabled"`
PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"`
CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"`
LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// ReviewMRTask records which MRs have already had tasks created (dedup).
type ReviewMRTask struct {
ID string `json:"id" db:"id"`
ReviewWatchID string `json:"review_watch_id" db:"review_watch_id"`
ProjectPath string `json:"project_path" db:"project_path"`
MRIID int `json:"mr_iid" db:"mr_iid"`
MRURL string `json:"mr_url" db:"mr_url"`
TaskID string `json:"task_id" db:"task_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// IssueWatch configures periodic polling for GitLab issues matching a query.
type IssueWatch struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
WorkflowID string `json:"workflow_id" db:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id" db:"workflow_step_id"`
Projects []ProjectFilter `json:"projects" db:"-"`
ProjectsJSON string `json:"-" db:"projects"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id" db:"executor_profile_id"`
Prompt string `json:"prompt" db:"prompt"`
Labels []string `json:"labels" db:"-"`
LabelsJSON string `json:"-" db:"labels"`
CustomQuery string `json:"custom_query" db:"custom_query"`
Enabled bool `json:"enabled" db:"enabled"`
PollIntervalSeconds int `json:"poll_interval_seconds" db:"poll_interval_seconds"`
CleanupPolicy string `json:"cleanup_policy" db:"cleanup_policy"`
LastPolledAt *time.Time `json:"last_polled_at,omitempty" db:"last_polled_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// IssueWatchTask records which issues have had tasks created (dedup).
type IssueWatchTask struct {
ID string `json:"id" db:"id"`
IssueWatchID string `json:"issue_watch_id" db:"issue_watch_id"`
ProjectPath string `json:"project_path" db:"project_path"`
IssueIID int `json:"issue_iid" db:"issue_iid"`
IssueURL string `json:"issue_url" db:"issue_url"`
TaskID string `json:"task_id" db:"task_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// CreateReviewWatchRequest is the request body for creating a review watch.
type CreateReviewWatchRequest struct {
WorkspaceID string `json:"workspace_id"`
WorkflowID string `json:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id"`
Projects []ProjectFilter `json:"projects"`
AgentProfileID string `json:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id"`
Prompt string `json:"prompt"`
ReviewScope string `json:"review_scope"`
CustomQuery string `json:"custom_query"`
PollIntervalSeconds int `json:"poll_interval_seconds"`
CleanupPolicy string `json:"cleanup_policy"`
}
// UpdateReviewWatchRequest is the request body for updating a review watch.
type UpdateReviewWatchRequest struct {
WorkflowID *string `json:"workflow_id,omitempty"`
WorkflowStepID *string `json:"workflow_step_id,omitempty"`
Projects *[]ProjectFilter `json:"projects,omitempty"`
AgentProfileID *string `json:"agent_profile_id,omitempty"`
ExecutorProfileID *string `json:"executor_profile_id,omitempty"`
Prompt *string `json:"prompt,omitempty"`
ReviewScope *string `json:"review_scope,omitempty"`
CustomQuery *string `json:"custom_query,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"`
CleanupPolicy *string `json:"cleanup_policy,omitempty"`
}
// CreateIssueWatchRequest is the request body for creating an issue watch.
type CreateIssueWatchRequest struct {
WorkspaceID string `json:"workspace_id"`
WorkflowID string `json:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id"`
Projects []ProjectFilter `json:"projects"`
AgentProfileID string `json:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id"`
Prompt string `json:"prompt"`
Labels []string `json:"labels"`
CustomQuery string `json:"custom_query"`
PollIntervalSeconds int `json:"poll_interval_seconds"`
CleanupPolicy string `json:"cleanup_policy"`
}
// UpdateIssueWatchRequest is the request body for updating an issue watch.
type UpdateIssueWatchRequest struct {
WorkflowID *string `json:"workflow_id,omitempty"`
WorkflowStepID *string `json:"workflow_step_id,omitempty"`
Projects *[]ProjectFilter `json:"projects,omitempty"`
AgentProfileID *string `json:"agent_profile_id,omitempty"`
ExecutorProfileID *string `json:"executor_profile_id,omitempty"`
Prompt *string `json:"prompt,omitempty"`
Labels *[]string `json:"labels,omitempty"`
CustomQuery *string `json:"custom_query,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
PollIntervalSeconds *int `json:"poll_interval_seconds,omitempty"`
CleanupPolicy *string `json:"cleanup_policy,omitempty"`
}
// MRFeedbackEvent is published when an MR has new feedback (new discussion,
// pipeline state change, approval state change). Used by the frontend to
// surface a toast / refresh the task.
type MRFeedbackEvent struct {
SessionID string `json:"session_id"`
TaskID string `json:"task_id"`
ProjectPath string `json:"project_path"`
MRIID int `json:"mr_iid"`
NewPipelineState string `json:"new_pipeline_state"`
NewApprovalState string `json:"new_approval_state"`
}
// NewReviewMREvent is published when a new MR needing review is found by a
// review watch and a task has been auto-created.
type NewReviewMREvent struct {
ReviewWatchID string `json:"review_watch_id"`
WorkspaceID string `json:"workspace_id"`
WorkflowID string `json:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id"`
AgentProfileID string `json:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id"`
Prompt string `json:"prompt"`
MR *MR `json:"mr"`
}
// NewIssueEvent is published when a new issue matching a watch is found.
type NewIssueEvent struct {
IssueWatchID string `json:"issue_watch_id"`
WorkspaceID string `json:"workspace_id"`
WorkflowID string `json:"workflow_id"`
WorkflowStepID string `json:"workflow_step_id"`
AgentProfileID string `json:"agent_profile_id"`
ExecutorProfileID string `json:"executor_profile_id"`
Prompt string `json:"prompt"`
Issue *Issue `json:"issue"`
}
// --- Stats ---
// Stats aggregates GitLab activity counts surfaced on the /gitlab page.
type Stats struct {
OpenMRs int `json:"open_mrs"`
MRsAwaitingMyReview int `json:"mrs_awaiting_my_review"`
OpenIssuesAssignedMe int `json:"open_issues_assigned_me"`
}
// --- Merge methods ---
// ProjectMergeMethods reports which merge methods a GitLab project allows.
// GitLab projects have a single `merge_method` setting; we expose it as a
// boolean set parallel to GitHub's allow_*_merge for the frontend.
type ProjectMergeMethods struct {
Merge bool `json:"merge"` // GitLab `merge_method=merge`
RebaseMerge bool `json:"rebase_merge"` // GitLab `merge_method=rebase_merge`
FastForward bool `json:"fast_forward"` // GitLab `merge_method=ff`
AllowSquash bool `json:"allow_squash"` // squash option on the merge request
}
// --- Action presets ---
// ActionPresetKind enumerates the two lists of quick-launch presets.
const (
ActionPresetKindMR = "mr"
ActionPresetKindIssue = "issue"
)
// ActionPreset is a single configurable quick-task launcher entry.
// PromptTemplate supports `{{url}}` and `{{title}}` placeholders substituted
// client-side when the dialog is opened.
type ActionPreset struct {
ID string `json:"id"`
Label string `json:"label"`
Hint string `json:"hint"`
Icon string `json:"icon"`
PromptTemplate string `json:"prompt_template"`
}
// ActionPresets groups the MR and Issue preset lists for a workspace.
type ActionPresets struct {
WorkspaceID string `json:"workspace_id"`
MR []ActionPreset `json:"mr"`
Issue []ActionPreset `json:"issue"`
}
// UpdateActionPresetsRequest replaces one or both preset lists for a workspace.
// Nil fields leave the corresponding list unchanged.
type UpdateActionPresetsRequest struct {
WorkspaceID string `json:"workspace_id"`
MR *[]ActionPreset `json:"mr,omitempty"`
Issue *[]ActionPreset `json:"issue,omitempty"`
}
// DefaultMRActionPresets returns the built-in MR presets used when a workspace
// has no stored overrides.
func DefaultMRActionPresets() []ActionPreset {
return []ActionPreset{
{
ID: "review",
Label: "Review",
Hint: "Read the diff, flag issues",
Icon: "eye",
PromptTemplate: "Review the merge request at {{url}}. Provide feedback on code quality, correctness, and suggest improvements.",
},
{
ID: "address_feedback",
Label: "Address feedback",
Hint: "Apply review comments",
Icon: "message",
PromptTemplate: "Review the feedback on the merge request at {{url}}. Evaluate each comment critically — apply changes that improve the code, push back on suggestions that are unnecessary or harmful, and explain your reasoning. Push the changes when done.",
},
{
ID: "fix_ci",
Label: "Fix pipeline",
Hint: "Diagnose failing jobs",
Icon: "tool",
PromptTemplate: "Investigate and fix the failing pipeline jobs and merge conflicts on the merge request at {{url}}. Run the failing jobs locally, resolve any conflicts, diagnose issues, and push fixes.",
},
}
}
// DefaultIssueActionPresets returns the built-in Issue presets used when a
// workspace has no stored overrides.
func DefaultIssueActionPresets() []ActionPreset {
return []ActionPreset{
{
ID: "implement",
Label: "Implement",
Hint: "Build and open an MR",
Icon: "code",
PromptTemplate: `Implement the changes described in the GitLab issue at {{url}} (title: "{{title}}"). Open a merge request when complete.`,
},
{
ID: "investigate",
Label: "Investigate",
Hint: "Find the root cause",
Icon: "search",
PromptTemplate: `Investigate the GitLab issue at {{url}} (title: "{{title}}"). Identify root cause and summarize findings.`,
},
{
ID: "reproduce",
Label: "Reproduce",
Hint: "Document repro steps",
Icon: "bug",
PromptTemplate: `Reproduce the bug described in the GitLab issue at {{url}} (title: "{{title}}"). Document the reproduction steps.`,
},
}
}