Skip to content

Commit 77aa6ee

Browse files
authored
Merge pull request #581 from srtab/feat/quick-actions-refactor
Refactored quick actions to be more like commands.
2 parents 18ccafa + 15b69af commit 77aa6ee

37 files changed

+712
-997
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ exclude_lines =
2626
# Don't complain if tests don't hit defensive assertion code:
2727
raise AssertionError
2828
raise NotImplementedError
29+
if TYPE_CHECKING:

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- Refactored repository configuration file schema to be more flexible and easier to use. **BREAKING CHANGE**
4141
- Moved tools from `daiv/automation/tools` to `daiv/automation/agents/tools`.
4242
- Moved quick actions from `daiv/automation/quick_actions` to `daiv/quick_actions`.
43+
- Migrated quick action `help` to activate as `@daiv /help` instead of `@daiv help`. **BREAKING CHANGE**
44+
- Migrated quick action `plan execute` to activate as `@daiv /approve-plan` instead of `@daiv plan execute`. **BREAKING CHANGE**
45+
- Migrated quick action `plan revise` to activate as `@daiv /revise-plan` instead of `@daiv plan revise`. **BREAKING CHANGE**
4346
- Updated project dependencies.
4447
- Updated documentation.
4548

daiv/automation/agents/review_addressor/prompts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
### Step 2 • Final reply shown to the reviewer
9797
Immediately emit plain text (no phases, no tool names):
9898
- First-person voice ("I suggest…", "I noticed…").
99-
- Match the reviewers language only if detection ≥80% confidence; otherwise use English.
99+
- Match the reviewer's language only if detection ≥80% confidence; otherwise use English.
100100
- Be technically precise; reference code generically or link to exact lines (e.g., [`src/module/file.ts#L120-L135`](src/module/file.ts#L120-L135)).
101101
- Keep it concise yet complete; include a tiny snippet (≤ 8 lines) **only if it materially clarifies or shows the fix**.
102102
- If static analysis is insufficient, say so briefly and propose one minimal next step (optionally append **`(confidence: low/med/high)`**).

daiv/codebase/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ class Discussion(BaseModel):
199199
id: str
200200
resolve_id: str | None = None # The id of the comment to resolve, only used for GitHub.
201201
notes: list[Note] = Field(default_factory=list)
202-
is_reply: bool = False
203202
is_thread: bool = False
204203
is_resolvable: bool = False
205204

daiv/codebase/clients/base.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,23 @@ def get_issue(self, repo_id: str, issue_id: int) -> Issue:
114114
pass
115115

116116
@abc.abstractmethod
117-
def create_issue_comment(self, repo_id: str, issue_id: int, body: str) -> str | None:
117+
def create_issue_comment(
118+
self, repo_id: str, issue_id: int, body: str, reply_to_id: str | None = None, as_thread: bool = False
119+
) -> str | None:
118120
pass
119121

120122
@abc.abstractmethod
121-
def update_issue_comment(self, repo_id: str, issue_id: int, comment_id: int, body: str) -> str | None:
123+
def update_issue_comment(
124+
self, repo_id: str, issue_id: int, comment_id: int, body: str, reply_to_id: str | None = None
125+
) -> str | None:
122126
pass
123127

124128
@abc.abstractmethod
125129
def create_issue_note_emoji(self, repo_id: str, issue_id: int, emoji: Emoji, note_id: str):
126130
pass
127131

128132
@abc.abstractmethod
129-
def get_issue_discussion(
130-
self, repo_id: str, issue_id: int, discussion_id: str, only_resolvable: bool = True
131-
) -> Discussion:
133+
def get_issue_comment(self, repo_id: str, issue_id: int, comment_id: str) -> Discussion:
132134
pass
133135

134136
@abc.abstractmethod
@@ -137,16 +139,6 @@ def get_issue_related_merge_requests(
137139
) -> list[MergeRequest]:
138140
pass
139141

140-
@abc.abstractmethod
141-
def create_issue_discussion_note(
142-
self, repo_id: str, issue_id: int, body: str, discussion_id: str | None = None
143-
) -> str | None:
144-
pass
145-
146-
@abc.abstractmethod
147-
def update_issue_discussion_note(self, repo_id: str, issue_id: int, discussion_id: str, note_id: str, body: str):
148-
pass
149-
150142
@abc.abstractmethod
151143
@cached_property
152144
def current_user(self) -> User:
@@ -161,17 +153,15 @@ def get_merge_request_latest_pipelines(self, repo_id: str, merge_request_id: int
161153
pass
162154

163155
@abc.abstractmethod
164-
def get_merge_request_discussion(
165-
self, repo_id: str, merge_request_id: int, discussion_id: str, only_resolvable: bool = True
166-
) -> Discussion:
156+
def get_merge_request_review_comments(self, repo_id: str, merge_request_id: int) -> list[Discussion]:
167157
pass
168158

169159
@abc.abstractmethod
170-
def get_merge_request_review_comments(self, repo_id: str, merge_request_id: int) -> list[Discussion]:
160+
def get_merge_request_comments(self, repo_id: str, merge_request_id: int) -> list[Discussion]:
171161
pass
172162

173163
@abc.abstractmethod
174-
def get_merge_request_comments(self, repo_id: str, merge_request_id: int) -> list[Discussion]:
164+
def get_merge_request_comment(self, repo_id: str, merge_request_id: int, comment_id: str) -> Discussion:
175165
pass
176166

177167
@abc.abstractmethod

daiv/codebase/clients/github/api/callbacks.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from quick_actions.base import Scope
77
from quick_actions.parser import QuickActionCommand, parse_quick_action
88
from quick_actions.registry import quick_action_registry
9-
from quick_actions.tasks import execute_quick_action_task
9+
from quick_actions.tasks import execute_issue_task, execute_merge_request_task
1010

1111
from codebase.api.callbacks import BaseCallback
1212
from codebase.clients import RepoClient
@@ -100,8 +100,6 @@ def accept_callback(self) -> bool:
100100
async def process_callback(self):
101101
"""
102102
Trigger the task to address the review feedback or issue comment like the plan approval use case.
103-
104-
GitLab Note Webhook is called multiple times, one per note/discussion.
105103
"""
106104
if self._is_quick_action:
107105
logger.info("Found quick action in note: '%s'", self._quick_action_command.raw)
@@ -110,18 +108,26 @@ async def process_callback(self):
110108
self.repository.full_name, self.issue.number, Emoji.THUMBSUP, self.comment.id
111109
)
112110

113-
await sync_to_async(
114-
execute_quick_action_task.si(
115-
repo_id=self.repository.full_name,
116-
discussion_id="", # GitHub doesn't have discussions like GitLab.
117-
note_id=self.comment.id,
118-
issue_id=self.issue.number,
119-
merge_request_id=None,
120-
action_verb=self._quick_action_command.verb,
121-
action_args=" ".join(self._quick_action_command.args),
122-
action_scope=Scope.ISSUE,
123-
).delay
124-
)()
111+
if self._action_scope == Scope.ISSUE:
112+
await sync_to_async(
113+
execute_issue_task.si(
114+
repo_id=self.repository.full_name,
115+
comment_id=self.comment.id,
116+
action_command=self._quick_action_command.command,
117+
action_args=" ".join(self._quick_action_command.args),
118+
issue_id=self.issue.number,
119+
).delay
120+
)()
121+
elif self._action_scope == Scope.MERGE_REQUEST:
122+
await sync_to_async(
123+
execute_merge_request_task.si(
124+
repo_id=self.repository.full_name,
125+
comment_id=self.comment.id,
126+
action_command=self._quick_action_command.command,
127+
action_args=" ".join(self._quick_action_command.args),
128+
merge_request_id=self.issue.number,
129+
).delay
130+
)()
125131

126132
elif self._is_merge_request_review:
127133
# The webhook doesn't provide the source branch, so we need to fetch it from the merge request.
@@ -168,25 +174,36 @@ def _quick_action_command(self) -> QuickActionCommand | None:
168174
if not quick_action_command:
169175
return None
170176

171-
action_classes = quick_action_registry.get_actions(verb=quick_action_command.verb, scope=Scope.ISSUE)
177+
action_classes = quick_action_registry.get_actions(
178+
command=quick_action_command.command, scope=self._action_scope
179+
)
172180

173181
if not action_classes:
174182
logger.warning(
175-
"Quick action '%s' not found in registry for scope '%s'", quick_action_command.verb, Scope.ISSUE
183+
"Quick action '%s' not found in registry for scope '%s'",
184+
quick_action_command.command,
185+
self._action_scope,
176186
)
177187
return None
178188

179189
if len(action_classes) > 1:
180190
logger.warning(
181191
"Multiple quick actions found for '%s' in registry for scope '%s': %s",
182-
quick_action_command.verb,
183-
Scope.ISSUE,
184-
[a.verb for a in action_classes],
192+
quick_action_command.command,
193+
self._action_scope,
194+
[a.command for a in action_classes],
185195
)
186196
return None
187197

188198
return quick_action_command
189199

200+
@property
201+
def _action_scope(self) -> Scope:
202+
"""
203+
Get the scope of the quick action.
204+
"""
205+
return Scope.MERGE_REQUEST if self.issue.is_pull_request() else Scope.ISSUE
206+
190207

191208
class PullRequestReviewCallback(GitHubCallback):
192209
"""

daiv/codebase/clients/github/client.py

Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -199,21 +199,6 @@ def get_issue(self, repo_id: str, issue_id: int) -> Issue:
199199
labels=[label.name for label in issue.labels],
200200
)
201201

202-
def create_issue_comment(self, repo_id: str, issue_id: int, body: str) -> str | None:
203-
"""
204-
Comment on an issue.
205-
206-
Returns:
207-
The comment ID.
208-
"""
209-
return self.client.get_repo(repo_id, lazy=True).get_issue(issue_id).create_comment(body).id
210-
211-
def update_issue_comment(self, repo_id: str, issue_id: int, comment_id: int, body: str):
212-
"""
213-
Update a comment on an issue.
214-
"""
215-
self.client.get_repo(repo_id, lazy=True).get_issue(issue_id).get_comment(comment_id).edit(body)
216-
217202
def create_issue_note_emoji(self, repo_id: str, issue_id: int, emoji: Emoji, note_id: str):
218203
"""
219204
Create an emoji in a note of an issue.
@@ -225,56 +210,56 @@ def create_issue_note_emoji(self, repo_id: str, issue_id: int, emoji: Emoji, not
225210
emoji_reaction
226211
)
227212

228-
def get_issue_discussion(
229-
self, repo_id: str, issue_id: int, discussion_id: str, only_resolvable: bool = True
230-
) -> Discussion:
213+
def get_issue_comment(self, repo_id: str, issue_id: int, comment_id: str) -> Discussion:
231214
"""
232-
Get a discussion from an issue.
215+
Get a comment from an issue.
233216
234-
For GitHub, there's no distinction between discussions and notes.
217+
For GitHub, there's no distinction between comments and notes.
235218
236219
Args:
237220
repo_id: The repository ID.
238221
issue_id: The issue ID.
239-
discussion_id: The discussion ID. This is not used for GitHub.
240-
only_resolvable: Whether to only return resolvable notes. This is not used for GitHub.
222+
comment_id: The comment ID.
241223
242224
Returns:
243225
The discussion object.
244226
"""
245227
issue = self.client.get_repo(repo_id, lazy=True).get_issue(issue_id)
246228
# GitHub doesn't have discussions like GitLab. This is a workaround to get the notes of an issue.
247-
return Discussion(id=discussion_id, notes=self._serialize_comments(issue.get_comments()), is_reply=False)
229+
return Discussion(id=comment_id, notes=self._serialize_comments(issue.get_comments()))
248230

249-
def create_issue_discussion_note(
250-
self, repo_id: str, issue_id: int, body: str, discussion_id: str | None = None
231+
def create_issue_comment(
232+
self, repo_id: str, issue_id: int, body: str, reply_to_id: str | None = None, as_thread: bool = False
251233
) -> str | None:
252234
"""
253-
Create a comment on an issue.
254-
255-
For GitHub, there's no distinction between discussions and notes. This method creates a comment on the issue.
235+
Comment on an issue.
256236
257237
Args:
258238
repo_id: The repository ID.
259239
issue_id: The issue ID.
260240
body: The comment body.
261-
discussion_id: The discussion ID. This is not used for GitHub.
241+
reply_to_id: The ID of the comment to reply to. This is not supported for GitHub.
242+
as_thread: Whether to create a thread. This is not supported for GitHub.
243+
244+
Returns:
245+
The comment ID.
262246
"""
263-
# GitHub doesn't have discussions like GitLab. This is a workaround to create a comment on the issue.
264-
return self.create_issue_comment(repo_id, issue_id, body)
247+
return self.client.get_repo(repo_id, lazy=True).get_issue(issue_id).create_comment(body).id
265248

266-
def update_issue_discussion_note(self, repo_id: str, issue_id: int, discussion_id: str, note_id: str, body: str):
249+
def update_issue_comment(
250+
self, repo_id: str, issue_id: int, comment_id: int, body: str, reply_to_id: str | None = None
251+
):
267252
"""
268253
Update a comment on an issue.
269254
270255
Args:
271256
repo_id: The repository ID.
272257
issue_id: The issue ID.
273-
discussion_id: The discussion ID. This is not used for GitHub.
274-
note_id: The note ID.
258+
comment_id: The comment ID.
275259
body: The comment body.
260+
reply_to_id: The ID of the comment to reply to. This is not supported for GitHub.
276261
"""
277-
self.update_issue_comment(repo_id, issue_id, note_id, body)
262+
self.client.get_repo(repo_id, lazy=True).get_issue(issue_id).get_comment(comment_id).edit(body)
278263

279264
@cached_property
280265
def current_user(self) -> User:
@@ -557,17 +542,14 @@ def get_merge_request_diff(self, repo_id: str, merge_request_id: int) -> PatchSe
557542
headers, data = self.client.requester.requestJsonAndCheck("GET", pr.diff_url, follow_302_redirect=True)
558543
return PatchSet.from_string(data["data"])
559544

560-
def get_merge_request_discussion(
561-
self, repo_id: str, merge_request_id: int, discussion_id: str, only_resolvable: bool = True
562-
) -> Discussion:
545+
def get_merge_request_comment(self, repo_id: str, merge_request_id: int, comment_id: str) -> Discussion:
563546
"""
564-
Get a discussion from a merge request.
547+
Get a comment from a merge request.
565548
566549
Args:
567550
repo_id: The repository ID.
568551
merge_request_id: The merge request ID.
569-
discussion_id: The discussion ID.
570-
only_resolvable: Whether to only return resolvable notes (review comments).
552+
comment_id: The comment ID.
571553
572554
Returns:
573555
The discussion object.
@@ -576,22 +558,14 @@ def get_merge_request_discussion(
576558

577559
comment = None
578560
try:
579-
comment = pr.get_issue_comment(discussion_id)
561+
comment = pr.get_issue_comment(comment_id)
580562
except UnknownObjectException:
581-
comment = pr.get_review_comment(discussion_id)
582-
583-
if only_resolvable:
584-
if isinstance(comment, IssueComment) and comment.reactions.get("rocket", 0) > 0:
585-
comment = None
586-
elif isinstance(comment, PullRequestComment):
587-
unresolved_comment_ids, _ = self._unresolved_comment_ids(repo_id, merge_request_id)
588-
if comment.id not in unresolved_comment_ids:
589-
comment = None
563+
comment = pr.get_review_comment(comment_id)
590564

591565
if comment is None:
592-
return Discussion(id=discussion_id, notes=[])
566+
return Discussion(id=comment_id, notes=[])
593567

594-
return Discussion(id=discussion_id, notes=self._serialize_comments([comment], from_merge_request=True))
568+
return Discussion(id=comment_id, notes=self._serialize_comments([comment], from_merge_request=True))
595569

596570
def get_merge_request_review_comments(self, repo_id: str, merge_request_id: int) -> list[Discussion]:
597571
"""

0 commit comments

Comments
 (0)