collavre 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 726c316625f342dc6d95769cc629900ea58c58a830d5a072fa9bd86609724e1f
4
- data.tar.gz: 629f17bdbc23f81f22c16a19b09ee6eaeab5013fc7e6de7096f5bcb6ff1ab3f7
3
+ metadata.gz: 22776a6f9413458cb9ff5c5c9518b1291939beab2b387c6e1e4d7f557a8585c4
4
+ data.tar.gz: 0dde98de3d1d7c8dc983df5ebcb89d6eb9d9851d5e8c473214def3bcdad7c954
5
5
  SHA512:
6
- metadata.gz: 26eba94cd79976a2d21587c18e20b12734021fc7e3c207e0bd76c62cf3e2e1b8c258c711cc9f95b07440a0947702a58f1628e5df457fea2921255606e2ec0230
7
- data.tar.gz: 547794c0208675a2a951320549ac26a07a01ea707ee1611a8d7a2dcf8d2f3475068b7e316d7de59037dfee0b03d1389b8671a191ee1c02e1798caed77e8d3e65
6
+ metadata.gz: 495868ccfef0655e1de79cddb77cb7321ce50cbb26774fa13f45bd55cc0d1f76dd61e6c426b1f88234fdb2167ff7b757b4ef07ee9a96b932b6fa742121a8c177
7
+ data.tar.gz: f2b130635ea439864cc07a6d4bac31b8a13a4b265659fbb1aae06450498d691c6fe3d9b55fbcd874cf23140716587bc16fd80d4c0214913aff0de7c0ff53d302
@@ -180,6 +180,73 @@ body.chat-fullscreen {
180
180
  margin-top: 0.2em;
181
181
  }
182
182
 
183
+ .comment-quoted-block {
184
+ margin-top: 0.3em;
185
+ margin-bottom: 0.2em;
186
+ }
187
+
188
+ .comment-quoted-text {
189
+ border-left: 3px solid var(--color-border);
190
+ padding: 0.3em 0.6em;
191
+ margin: 0;
192
+ color: var(--color-muted);
193
+ font-size: 0.9em;
194
+ background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
195
+ border-radius: 0 4px 4px 0;
196
+ white-space: pre-wrap;
197
+ word-break: break-word;
198
+ }
199
+
200
+ /* Quote indicator in form */
201
+ .comment-quote-indicator {
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ padding: 0.3em 0.6em;
206
+ margin-bottom: 0.3em;
207
+ background: color-mix(in srgb, var(--color-section-bg) 50%, transparent);
208
+ border-left: 3px solid var(--color-accent, #007bff);
209
+ border-radius: 0 4px 4px 0;
210
+ font-size: 0.85em;
211
+ color: var(--color-muted);
212
+ }
213
+
214
+ .comment-quote-indicator-text {
215
+ overflow: hidden;
216
+ text-overflow: ellipsis;
217
+ white-space: nowrap;
218
+ flex: 1;
219
+ min-width: 0;
220
+ }
221
+
222
+ .comment-quote-cancel {
223
+ border: none;
224
+ background: none;
225
+ cursor: pointer;
226
+ color: var(--color-muted);
227
+ font-size: 1.1em;
228
+ padding: 0 0.2em;
229
+ line-height: 1;
230
+ flex-shrink: 0;
231
+ }
232
+
233
+ .comment-quote-cancel:hover {
234
+ color: var(--color-text);
235
+ }
236
+
237
+ /* Review popup (uses common-popup) */
238
+ .comment-review-popup {
239
+ min-width: auto;
240
+ }
241
+
242
+ .comment-review-popup .common-popup-item {
243
+ padding: 0.3em 0.8em;
244
+ font-size: 0.85em;
245
+ cursor: pointer;
246
+ border-radius: 4px;
247
+ white-space: nowrap;
248
+ }
249
+
183
250
  .comment-reactions {
184
251
  margin-top: 0.4em;
185
252
  display: flex;
@@ -45,15 +45,7 @@ module Collavre
45
45
  end
46
46
 
47
47
  def broadcast_reaction_update
48
- payload = build_reaction_payload
49
- Turbo::StreamsChannel.broadcast_action_to(
50
- [ @creative, :comments ],
51
- action: "update_reactions",
52
- target: view_context.dom_id(@comment),
53
- attributes: {
54
- data: payload.to_json
55
- }
56
- )
48
+ CommentReaction.broadcast_reaction_update(@comment)
57
49
  end
58
50
 
59
51
  def set_creative
@@ -167,8 +167,9 @@ module Collavre
167
167
  comment: {
168
168
  id: @comment.id,
169
169
  content: @comment.content,
170
- user_id: @comment.user_id
171
- },
170
+ user_id: @comment.user_id,
171
+ quoted_comment_id: @comment.quoted_comment_id
172
+ }.compact,
172
173
  creative: {
173
174
  id: @creative.id,
174
175
  description: @creative.description
@@ -189,7 +190,7 @@ module Collavre
189
190
 
190
191
  def update
191
192
  if @comment.user == Current.user
192
- safe_params = comment_params
193
+ safe_params = comment_params.except(:quoted_comment_id, :quoted_text)
193
194
  if safe_params[:topic_id].present? && !@creative.topics.where(id: safe_params[:topic_id]).exists?
194
195
  render json: { error: I18n.t("collavre.comments.invalid_topic") }, status: :unprocessable_entity and return
195
196
  end
@@ -552,7 +553,7 @@ module Collavre
552
553
  end
553
554
 
554
555
  def comment_params
555
- params.require(:comment).permit(:content, :private, :topic_id, images: [])
556
+ params.require(:comment).permit(:content, :private, :topic_id, :quoted_comment_id, :quoted_text, images: [])
556
557
  end
557
558
 
558
559
  def can_convert_comment?
@@ -137,7 +137,10 @@ module Collavre
137
137
  end
138
138
 
139
139
  scope = if params[:scope] == "contacts" && Current.user
140
- Current.user.contact_users
140
+ # Include both contacts and searchable users (e.g., AI agents with searchable=true)
141
+ contact_ids = Current.user.contact_users.select(:id)
142
+ searchable_ids = Collavre::User.where(searchable: true).select(:id)
143
+ Collavre::User.where(id: contact_ids).or(Collavre::User.where(id: searchable_ids))
141
144
  else
142
145
  Collavre::User.mentionable_for(creative)
143
146
  end
@@ -12,7 +12,6 @@ import "./modules/plans_menu"
12
12
  import "./modules/inbox_panel"
13
13
  import "./modules/creative_guide"
14
14
  import "./modules/share_modal"
15
- import "./modules/share_user_popup"
16
15
  import "./modules/creative_row_editor"
17
16
  import "./modules/slide_view"
18
17
 
@@ -1,5 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { renderCommentMarkdown } from '../lib/utils/markdown'
3
+ import CommonPopup from '../lib/common_popup'
3
4
 
4
5
  // Connects to data-controller="comment"
5
6
  export default class extends Controller {
@@ -13,6 +14,10 @@ export default class extends Controller {
13
14
  contentElement.dataset.rendered = 'true'
14
15
  }
15
16
 
17
+ // Text selection quote support
18
+ this.handleMouseUp = this.handleMouseUp.bind(this)
19
+ this.element.addEventListener('mouseup', this.handleMouseUp)
20
+
16
21
  this.currentUserId = document.body.dataset.currentUserId
17
22
  const commentAuthorId = this.element.dataset.userId
18
23
  const creativeOwnerId = this.element.dataset.creativeOwnerId
@@ -108,6 +113,103 @@ export default class extends Controller {
108
113
  }
109
114
  }
110
115
 
116
+ disconnect() {
117
+ this.element.removeEventListener('mouseup', this.handleMouseUp)
118
+ this.hideReviewPopup()
119
+ if (this._reviewPopupEl) {
120
+ this._reviewPopupEl.remove()
121
+ this._reviewPopupEl = null
122
+ this._reviewPopup = null
123
+ }
124
+ }
125
+
126
+ handleMouseUp() {
127
+ // Small delay to let selection finalize
128
+ requestAnimationFrame(() => {
129
+ this.hideReviewPopup()
130
+
131
+ const selection = window.getSelection()
132
+ if (!selection || selection.isCollapsed) return
133
+
134
+ const selectedText = selection.toString().trim()
135
+ if (!selectedText) return
136
+
137
+ // Ensure selection is within this comment's content
138
+ const contentEl = this.element.querySelector('.comment-content')
139
+ if (!contentEl) return
140
+
141
+ const range = selection.getRangeAt(0)
142
+ if (!contentEl.contains(range.commonAncestorContainer)) return
143
+
144
+ // Show review popup below the selection using CommonPopup
145
+ const rect = range.getBoundingClientRect()
146
+ this.showReviewPopup(rect, selectedText)
147
+ })
148
+ }
149
+
150
+ showReviewPopup(anchorRect, selectedText) {
151
+ // Create or reuse popup element
152
+ if (!this._reviewPopupEl) {
153
+ const el = document.createElement('div')
154
+ el.className = 'common-popup comment-review-popup'
155
+ el.style.display = 'none'
156
+ el.style.padding = '0.3em'
157
+
158
+ const list = document.createElement('ul')
159
+ list.style.listStyle = 'none'
160
+ list.style.margin = '0'
161
+ list.style.padding = '0'
162
+ el.appendChild(list)
163
+
164
+ const commentsPopup = this.element.closest('#comments-popup')
165
+ if (commentsPopup) {
166
+ commentsPopup.appendChild(el)
167
+ } else {
168
+ document.body.appendChild(el)
169
+ }
170
+ this._reviewPopupEl = el
171
+ }
172
+
173
+ const reviewLabel = this.element.closest('#comments-popup')?.dataset?.reviewButtonText || 'Review'
174
+
175
+ this._reviewPopup = new CommonPopup(this._reviewPopupEl, {
176
+ onSelect: () => {
177
+ const commentId = this.element.dataset.commentId
178
+ const formController = this.findFormController()
179
+ if (formController) {
180
+ formController.quoteComment(commentId, selectedText)
181
+ }
182
+ window.getSelection().removeAllRanges()
183
+ this.hideReviewPopup()
184
+ },
185
+ renderItem: () => reviewLabel,
186
+ })
187
+
188
+ // Position below the selection end
189
+ const belowRect = {
190
+ left: anchorRect.left,
191
+ right: anchorRect.right,
192
+ top: anchorRect.bottom,
193
+ bottom: anchorRect.bottom + 4,
194
+ width: anchorRect.width,
195
+ }
196
+
197
+ this._reviewPopup.setItems([{ label: reviewLabel }])
198
+ this._reviewPopup.showAt(belowRect)
199
+ }
200
+
201
+ hideReviewPopup() {
202
+ if (this._reviewPopup) {
203
+ this._reviewPopup.hide()
204
+ }
205
+ }
206
+
207
+ findFormController() {
208
+ const popup = this.element.closest('#comments-popup')
209
+ if (!popup) return null
210
+ return this.application.getControllerForElementAndIdentifier(popup, 'comments--form')
211
+ }
212
+
111
213
  updateReactionsUI(reactionsData) {
112
214
  let reactionsContainer = this.element.querySelector('.comment-reactions')
113
215
 
@@ -14,6 +14,10 @@ export default class extends Controller {
14
14
  'imageInput',
15
15
  'imageButton',
16
16
  'attachmentList',
17
+ 'quotedCommentId',
18
+ 'quotedText',
19
+ 'quoteIndicator',
20
+ 'quoteIndicatorText',
17
21
  ]
18
22
 
19
23
  connect() {
@@ -154,6 +158,7 @@ export default class extends Controller {
154
158
  if (this.cancelTarget) this.cancelTarget.style.display = 'none'
155
159
  this.presenceController?.clearManualTypingMessage()
156
160
  this.clearImageAttachments()
161
+ this.cancelQuote()
157
162
  }
158
163
 
159
164
  setSendingState(isSending) {
@@ -499,6 +504,24 @@ export default class extends Controller {
499
504
  })
500
505
  }
501
506
 
507
+ quoteComment(commentId, selectedText) {
508
+ if (!commentId || !selectedText) return
509
+ this.quotedCommentIdTarget.value = commentId
510
+ this.quotedTextTarget.value = selectedText
511
+ this.quoteIndicatorTarget.style.display = ''
512
+ this.quoteIndicatorTextTarget.textContent = selectedText.length > 80
513
+ ? selectedText.substring(0, 80) + '…'
514
+ : selectedText
515
+ this.focusTextarea()
516
+ }
517
+
518
+ cancelQuote() {
519
+ this.quotedCommentIdTarget.value = ''
520
+ this.quotedTextTarget.value = ''
521
+ this.quoteIndicatorTarget.style.display = 'none'
522
+ this.quoteIndicatorTextTarget.textContent = ''
523
+ }
524
+
502
525
  renderCommentHtml(html, { replaceExisting = false } = {}) {
503
526
  const listElement = document.getElementById('comments-list')
504
527
  if (!listElement || !html) return
@@ -14,6 +14,7 @@ module Collavre
14
14
  belongs_to :approver, class_name: Collavre.configuration.user_class_name, optional: true
15
15
  belongs_to :action_executed_by, class_name: Collavre.configuration.user_class_name, optional: true
16
16
  belongs_to :topic, class_name: "Collavre::Topic", optional: true
17
+ belongs_to :quoted_comment, class_name: "Collavre::Comment", optional: true
17
18
  has_many :activity_logs, class_name: "Collavre::ActivityLog", dependent: :destroy
18
19
  has_many :comment_reactions, class_name: "Collavre::CommentReaction", dependent: :destroy
19
20
 
@@ -32,6 +33,10 @@ module Collavre
32
33
  after_update_commit :broadcast_update
33
34
  after_destroy_commit :broadcast_destroy, :broadcast_badges, :cancel_pending_tasks
34
35
 
36
+ def review_message?
37
+ quoted_comment_id.present?
38
+ end
39
+
35
40
  # public for db migration
36
41
  def creative_snippet
37
42
  creative.creative_snippet
@@ -7,5 +7,20 @@ module Collavre
7
7
 
8
8
  validates :emoji, presence: true, length: { maximum: 16 }
9
9
  validates :user_id, uniqueness: { scope: [ :comment_id, :emoji ] }
10
+
11
+ def self.broadcast_reaction_update(comment)
12
+ creative = comment.creative
13
+ reactions = comment.comment_reactions.reload.to_a
14
+ payload = reactions.group_by(&:emoji).map do |emoji, grouped|
15
+ { emoji: emoji, count: grouped.size, user_ids: grouped.map(&:user_id) }
16
+ end
17
+
18
+ Turbo::StreamsChannel.broadcast_action_to(
19
+ [ creative, :comments ],
20
+ action: "update_reactions",
21
+ target: "comment_#{comment.id}",
22
+ attributes: { data: payload.to_json }
23
+ )
24
+ end
10
25
  end
11
26
  end
@@ -16,6 +16,10 @@ module Collavre
16
16
  # Log start action
17
17
  log_action("start", { message: "Starting agent execution" })
18
18
 
19
+ # Set original comment early so build_messages can check review_eligible?
20
+ target_comment_id = @context.dig("comment", "id")
21
+ @original_comment = target_comment_id ? Comment.find_by(id: target_comment_id) : nil
22
+
19
23
  # Prepare messages for AI
20
24
  messages = build_messages
21
25
 
@@ -46,8 +50,6 @@ module Collavre
46
50
  rendered_system_prompt = "#{rendered_system_prompt}\n\n#{collaboration_prompt}" if collaboration_prompt.present?
47
51
 
48
52
  # Create a placeholder comment to stream into
49
- target_comment_id = @context.dig("comment", "id")
50
- @original_comment = target_comment_id ? Comment.find_by(id: target_comment_id) : nil
51
53
  @reply_comment = nil
52
54
 
53
55
  if @original_comment
@@ -101,13 +103,22 @@ module Collavre
101
103
  # Final save to ensure everything is consistent and trigger final callbacks
102
104
  if @reply_comment
103
105
  if @response_content.present?
104
- # Force dirty tracking since update_column bypassed it during streaming
105
- @reply_comment.content_will_change!
106
- @reply_comment.update!(content: @response_content)
107
- log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content })
108
-
109
- # Re-associate activity logs from the trigger comment to the reply comment
110
- reassociate_activity_logs(@original_comment, @reply_comment)
106
+ # Review message handling: update the quoted comment if agent has edit permission
107
+ if @original_comment&.review_message? && handle_review_message(@response_content)
108
+ # React to the review message with a completion emoji
109
+ add_review_completion_reaction(@original_comment)
110
+ # Preserve activity logs by moving them to the quoted comment before destroying placeholder
111
+ reassociate_activity_logs(@reply_comment, @original_comment.quoted_comment)
112
+ @reply_comment.destroy!
113
+ else
114
+ # Force dirty tracking since update_column bypassed it during streaming
115
+ @reply_comment.content_will_change!
116
+ @reply_comment.update!(content: @response_content)
117
+ log_action("reply_created", { comment_id: @reply_comment.id, content: @response_content })
118
+
119
+ # Re-associate activity logs from the trigger comment to the reply comment
120
+ reassociate_activity_logs(@original_comment, @reply_comment)
121
+ end
111
122
  else
112
123
  @reply_comment.destroy!
113
124
  end
@@ -223,11 +234,68 @@ module Collavre
223
234
  end
224
235
 
225
236
  payload_text = @context.dig("comment", "content") || @context.to_json
237
+
238
+ # Add review context only when the review will actually result in an in-place update.
239
+ # This prevents misleading the AI when quoting non-agent or ineligible comments.
240
+ if review_eligible?
241
+ quoted_body = @original_comment.quoted_comment&.content
242
+ review_context = I18n.t("collavre.ai_agent.review.context")
243
+ review_parts = [ review_context ]
244
+ review_parts << "---\nOriginal message:\n#{quoted_body}\n---" if quoted_body.present?
245
+ payload_text = "#{review_parts.join("\n\n")}\n\n#{payload_text}"
246
+ end
247
+
226
248
  messages << { role: "user", parts: [ { text: payload_text } ] }
227
249
 
228
250
  messages
229
251
  end
230
252
 
253
+ def review_eligible?
254
+ return false unless @original_comment&.review_message?
255
+
256
+ quoted_comment = @original_comment.quoted_comment
257
+ return false unless quoted_comment
258
+ return false unless quoted_comment.user_id == @agent.id
259
+ return false unless quoted_comment.creative_id == @original_comment.creative_id
260
+ # Ensure the quoted comment is not private (prevent privilege bypass via client-supplied IDs)
261
+ return false if quoted_comment.private?
262
+ # Ensure both comments are in the same topic to prevent cross-topic overwrites
263
+ return false unless quoted_comment.topic_id == @original_comment.topic_id
264
+
265
+ true
266
+ end
267
+
268
+ def handle_review_message(response_content)
269
+ return false unless review_eligible?
270
+
271
+ quoted_comment = @original_comment.quoted_comment
272
+ quoted_comment.update!(content: response_content)
273
+ log_action("review_updated", {
274
+ quoted_comment_id: quoted_comment.id,
275
+ original_comment_id: @original_comment.id,
276
+ content: response_content
277
+ })
278
+ true
279
+ end
280
+
281
+ def add_review_completion_reaction(comment)
282
+ reaction = begin
283
+ CommentReaction.find_or_create_by!(
284
+ comment: comment,
285
+ user: @agent,
286
+ emoji: "✅"
287
+ )
288
+ rescue ActiveRecord::RecordNotUnique
289
+ # Already reacted via race condition, fetch existing
290
+ CommentReaction.find_by(comment: comment, user: @agent, emoji: "✅")
291
+ end
292
+
293
+ CommentReaction.broadcast_reaction_update(comment) if reaction
294
+ rescue StandardError => e
295
+ # Reaction is supplementary; log but don't fail the review update
296
+ Rails.logger.warn("[AiAgentService] Failed to add review reaction: #{e.message}")
297
+ end
298
+
231
299
  def reply_to_comment(comment_id, content)
232
300
  original_comment = Comment.find_by(id: comment_id)
233
301
  return unless original_comment
@@ -43,7 +43,7 @@ module Collavre
43
43
  "token_spike_window_minutes" => 10
44
44
  },
45
45
  "stuck_detection" => {
46
- "enabled" => true,
46
+ "enabled" => false,
47
47
  "task_stuck_threshold_minutes" => 30, # Task running for > N minutes
48
48
  "creative_stall_threshold_minutes" => 120, # Creative no progress for > N minutes
49
49
  "create_system_comment" => true # Create system comment on escalation
@@ -267,7 +267,7 @@ module Collavre
267
267
  [
268
268
  "collavre.stuck_detection.creative_stalled",
269
269
  {
270
- creative_title: creative.description&.truncate(50) || "Untitled",
270
+ creative_title: creative.creative_snippet,
271
271
  hours: hours_stuck,
272
272
  progress: ((creative.progress || 0) * 100).round
273
273
  }
@@ -66,6 +66,11 @@
66
66
  <%= t('collavre.comments.delete_button') %>
67
67
  </button>
68
68
  </div>
69
+ <% if comment.quoted_text.present? %>
70
+ <div class="comment-quoted-block">
71
+ <blockquote class="comment-quoted-text"><%= comment.quoted_text %></blockquote>
72
+ </div>
73
+ <% end %>
69
74
  <div class="comment-content"><%= comment.content %></div>
70
75
 
71
76
 
@@ -31,7 +31,8 @@
31
31
  data-move-error-text="<%= t('collavre.comments.move_error') %>"
32
32
  data-hint-drag-topic-text="<%= t('collavre.comments.hint_drag_topic') %>"
33
33
  data-hint-move-button-text="<%= t('collavre.comments.hint_move_button') %>"
34
- data-add-participant-text="<%= t('collavre.comments.add_participant') %>">
34
+ data-add-participant-text="<%= t('collavre.comments.add_participant') %>"
35
+ data-review-button-text="<%= t('collavre.comments.review_button') %>">
35
36
  <div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
36
37
  <div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
37
38
  <div class="comments-popup-header">
@@ -62,6 +63,12 @@
62
63
  <div id="comments-list" data-comments--popup-target="list" data-comments--list-target="list"><%= t('app.loading') %></div>
63
64
  <div id="typing-indicator" data-comments--presence-target="typingIndicator"></div>
64
65
  <form id="new-comment-form" data-comments--popup-target="form" data-comments--form-target="form" style="display:none;">
66
+ <input type="hidden" name="comment[quoted_comment_id]" data-comments--form-target="quotedCommentId" value="" />
67
+ <input type="hidden" name="comment[quoted_text]" data-comments--form-target="quotedText" value="" />
68
+ <div class="comment-quote-indicator" data-comments--form-target="quoteIndicator" style="display:none;">
69
+ <span class="comment-quote-indicator-text" data-comments--form-target="quoteIndicatorText"></span>
70
+ <button type="button" class="comment-quote-cancel" data-action="click->comments--form#cancelQuote" title="<%= t('app.cancel') %>">&times;</button>
71
+ </div>
65
72
  <textarea class="shared-input-surface" name="comment[content]" data-comments--form-target="textarea" data-comments--presence-target="textarea" data-comments--mention-menu-target="textarea" rows="2" enterkeyhint="send"></textarea>
66
73
  <div class="comment-bottom">
67
74
  <input type="file" id="comment-images" name="comment[images][]" accept="image/*" multiple data-comments--form-target="imageInput" style="display:none;" />
@@ -202,8 +202,6 @@
202
202
 
203
203
  <%= render 'set_plan_modal' %>
204
204
  <% if can_manage_integrations %>
205
- <%# Legacy hardcoded integration modals %>
206
- <%= render 'collavre_github/integrations/modal', creative: current_creative %>
207
205
  <%# Dynamically registered integration modals %>
208
206
  <%= render 'integration_modals', creative: current_creative %>
209
207
  <% end %>
@@ -23,6 +23,8 @@ en:
23
23
  focus_instruction: "- Focus on answering the request"
24
24
  completion_instruction: "- Clearly communicate results when done"
25
25
  followup_instruction: "- If you need more information, ask the requester via @mention"
26
+ review:
27
+ context: "The user is reviewing your previous message and requesting changes. Your response will replace the original message. Provide a complete rewrite."
26
28
  collaboration:
27
29
  header: "## Available Agents for Collaboration"
28
30
  escalation_header: "### Escalation Targets (when stuck)"
@@ -23,6 +23,8 @@ ko:
23
23
  focus_instruction: "- 요청에 집중해서 답변하세요"
24
24
  completion_instruction: "- 완료되면 결과를 명확히 전달하세요"
25
25
  followup_instruction: "- 추가 정보가 필요하면 요청자에게 @멘션으로 물어보세요"
26
+ review:
27
+ context: "사용자가 당신의 이전 메시지를 리뷰하고 변경을 요청하고 있습니다. 당신의 응답이 원본 메시지를 대체합니다. 전체 내용을 다시 작성해주세요."
26
28
  collaboration:
27
29
  header: "## 협업 가능한 Agent"
28
30
  escalation_header: "### 에스컬레이션 대상 (문제 해결 불가 시)"
@@ -65,6 +65,7 @@ en:
65
65
  voice_stop: Stop
66
66
  speech_unavailable: Speech recognition is not supported in this browser.
67
67
  move_button: Move
68
+ review_button: Review
68
69
  move_no_selection: Select at least one message to move.
69
70
  move_error: Unable to move messages.
70
71
  add_participant: Add user
@@ -62,6 +62,7 @@ ko:
62
62
  voice_stop: 중지
63
63
  speech_unavailable: 이 브라우저는 음성 인식을 지원하지 않습니다.
64
64
  move_button: 이동
65
+ review_button: 리뷰
65
66
  move_no_selection: 이동할 메시지를 선택해주세요.
66
67
  move_error: 메시지를 이동할 수 없습니다.
67
68
  add_participant: 사용자 추가
@@ -0,0 +1,7 @@
1
+ class AddQuotedCommentToComments < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :comments, :quoted_comment_id, :integer
4
+ add_column :comments, :quoted_text, :text
5
+ add_index :comments, :quoted_comment_id
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Collavre
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collavre
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Collavre
@@ -577,6 +577,7 @@ files:
577
577
  - db/migrate/20260206005035_create_orchestrator_policies.rb
578
578
  - db/migrate/20260206094509_add_retry_count_to_tasks.rb
579
579
  - db/migrate/20260206100000_add_topic_id_to_tasks.rb
580
+ - db/migrate/20260212011655_add_quoted_comment_to_comments.rb
580
581
  - lib/collavre.rb
581
582
  - lib/collavre/configuration.rb
582
583
  - lib/collavre/engine.rb