collavre 0.3.2 → 0.5.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/collavre/actiontext.css +73 -71
  3. data/app/assets/stylesheets/collavre/activity_logs.css +18 -45
  4. data/app/assets/stylesheets/collavre/comments_popup.css +197 -35
  5. data/app/assets/stylesheets/collavre/creatives.css +101 -51
  6. data/app/assets/stylesheets/collavre/dark_mode.css +221 -88
  7. data/app/assets/stylesheets/collavre/design_tokens.css +334 -0
  8. data/app/assets/stylesheets/collavre/mention_menu.css +13 -9
  9. data/app/assets/stylesheets/collavre/popup.css +57 -27
  10. data/app/assets/stylesheets/collavre/slide_view.css +6 -6
  11. data/app/assets/stylesheets/collavre/user_menu.css +4 -5
  12. data/app/components/collavre/plans_timeline_component.html.erb +2 -2
  13. data/app/controllers/collavre/admin/orchestration_controller.rb +9 -2
  14. data/app/controllers/collavre/admin/settings_controller.rb +199 -0
  15. data/app/controllers/collavre/comments/reactions_controller.rb +1 -9
  16. data/app/controllers/collavre/comments_controller.rb +39 -162
  17. data/app/controllers/collavre/creatives_controller.rb +18 -58
  18. data/app/controllers/collavre/users_controller.rb +31 -3
  19. data/app/helpers/collavre/application_helper.rb +97 -0
  20. data/app/helpers/collavre/creatives_helper.rb +10 -202
  21. data/app/javascript/collavre.js +0 -1
  22. data/app/javascript/components/creative_tree_row.js +3 -2
  23. data/app/javascript/controllers/comment_controller.js +309 -4
  24. data/app/javascript/controllers/comments/form_controller.js +52 -0
  25. data/app/javascript/controllers/comments/presence_controller.js +13 -0
  26. data/app/javascript/controllers/creatives/tree_controller.js +2 -1
  27. data/app/javascript/controllers/link_creative_controller.js +29 -3
  28. data/app/javascript/lib/__tests__/html_code_block_wrapper.test.js +201 -0
  29. data/app/javascript/lib/html_code_block_wrapper.js +168 -0
  30. data/app/javascript/lib/utils/markdown.js +2 -1
  31. data/app/javascript/modules/creative_row_editor.js +5 -1
  32. data/app/javascript/utils/emoji_parser.js +21 -0
  33. data/app/jobs/collavre/ai_agent_job.rb +6 -2
  34. data/app/jobs/collavre/cron_action_job.rb +18 -6
  35. data/app/jobs/collavre/cron_scheduler_job.rb +112 -0
  36. data/app/models/collavre/comment/approvable.rb +50 -0
  37. data/app/models/collavre/comment/broadcastable.rb +119 -0
  38. data/app/models/collavre/comment/notifiable.rb +111 -0
  39. data/app/models/collavre/comment.rb +13 -258
  40. data/app/models/collavre/comment_reaction.rb +15 -0
  41. data/app/models/collavre/creative/describable.rb +86 -0
  42. data/app/models/collavre/creative/linkable.rb +77 -0
  43. data/app/models/collavre/creative/permissible.rb +103 -0
  44. data/app/models/collavre/creative.rb +3 -289
  45. data/app/models/collavre/orchestrator_policy.rb +1 -1
  46. data/app/models/collavre/system_setting.rb +27 -1
  47. data/app/models/collavre/user.rb +42 -0
  48. data/app/models/collavre/user_theme.rb +10 -0
  49. data/app/services/collavre/ai_agent/approval_handler.rb +110 -0
  50. data/app/services/collavre/ai_agent/message_builder.rb +129 -0
  51. data/app/services/collavre/ai_agent/review_handler.rb +70 -0
  52. data/app/services/collavre/ai_agent_service.rb +93 -150
  53. data/app/services/collavre/ai_client.rb +23 -4
  54. data/app/services/collavre/auto_theme_generator.rb +168 -50
  55. data/app/services/collavre/command_menu_service.rb +70 -0
  56. data/app/services/collavre/comment_move_service.rb +94 -0
  57. data/app/services/collavre/comments/action_executor.rb +10 -0
  58. data/app/services/collavre/comments/mcp_command.rb +1 -2
  59. data/app/services/collavre/creatives/create_service.rb +86 -0
  60. data/app/services/collavre/creatives/destroy_service.rb +41 -0
  61. data/app/services/collavre/creatives/index_query.rb +3 -0
  62. data/app/services/collavre/markdown_converter.rb +240 -0
  63. data/app/services/collavre/mention_parser.rb +63 -0
  64. data/app/services/collavre/orchestration/agent_context_builder.rb +24 -8
  65. data/app/services/collavre/orchestration/agent_orchestrator.rb +59 -10
  66. data/app/services/collavre/orchestration/loop_breaker.rb +12 -7
  67. data/app/services/collavre/orchestration/policy_resolver.rb +16 -2
  68. data/app/services/collavre/orchestration/scheduler.rb +4 -3
  69. data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
  70. data/app/services/collavre/system_events/context_builder.rb +1 -6
  71. data/app/services/collavre/tools/creative_batch_service.rb +107 -0
  72. data/app/services/collavre/tools/creative_update_service.rb +17 -12
  73. data/app/services/collavre/tools/cron_create_service.rb +17 -5
  74. data/app/views/admin/shared/_tabs.html.erb +2 -1
  75. data/app/views/collavre/admin/orchestration/show.html.erb +11 -0
  76. data/app/views/collavre/admin/settings/_system_tab.html.erb +138 -0
  77. data/app/views/collavre/admin/settings/_uiux_tab.html.erb +44 -0
  78. data/app/views/collavre/admin/settings/index.html.erb +11 -0
  79. data/app/views/collavre/admin/settings/uiux.html.erb +11 -0
  80. data/app/views/collavre/comments/_comment.html.erb +15 -5
  81. data/app/views/collavre/comments/_comments_popup.html.erb +9 -2
  82. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +0 -3
  83. data/app/views/collavre/creatives/_share_button.html.erb +0 -52
  84. data/app/views/collavre/creatives/_share_modal.html.erb +52 -0
  85. data/app/views/collavre/creatives/index.html.erb +5 -8
  86. data/app/views/collavre/shared/navigation/_panels.html.erb +2 -2
  87. data/app/views/collavre/user_themes/index.html.erb +7 -9
  88. data/app/views/collavre/users/_contact_management.html.erb +2 -1
  89. data/app/views/collavre/users/edit_ai.html.erb +7 -0
  90. data/app/views/collavre/users/index.html.erb +16 -1
  91. data/app/views/collavre/users/new_ai.html.erb +18 -8
  92. data/app/views/collavre/users/passkeys.html.erb +1 -1
  93. data/app/views/collavre/users/show.html.erb +1 -1
  94. data/app/views/layouts/collavre/slide.html.erb +8 -1
  95. data/config/locales/admin.en.yml +88 -0
  96. data/config/locales/admin.ko.yml +88 -0
  97. data/config/locales/ai_agent.en.yml +5 -1
  98. data/config/locales/ai_agent.ko.yml +5 -1
  99. data/config/locales/comments.en.yml +5 -1
  100. data/config/locales/comments.ko.yml +5 -1
  101. data/config/locales/orchestration.en.yml +8 -0
  102. data/config/locales/orchestration.ko.yml +8 -0
  103. data/config/locales/users.en.yml +12 -0
  104. data/config/locales/users.ko.yml +12 -0
  105. data/config/routes.rb +7 -1
  106. data/db/migrate/20260212011655_add_quoted_comment_to_comments.rb +7 -0
  107. data/db/migrate/20260213044247_add_agent_conf_to_users.rb +5 -0
  108. data/lib/collavre/engine.rb +25 -0
  109. data/lib/collavre/version.rb +1 -1
  110. metadata +32 -1
@@ -0,0 +1,52 @@
1
+ <!-- Share Creative Modal -->
2
+ <div id="share-creative-modal" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;align-items:center;justify-content:center;" data-creative-id="<%= (@parent_creative || @creative).id %>">
3
+ <div class="popup-box" style="min-width:320px;max-width:90vw;">
4
+ <button type="button" id="close-share-modal" class="popup-close-btn">&times;</button>
5
+ <h2><%= t('collavre.creatives.index.share_creative') %></h2>
6
+ <form id="share-creative-form" action="<%= collavre.creative_creative_shares_path(@parent_creative || @creative) %>" method="post" data-controller="share-invite">
7
+ <%= csrf_meta_tags %>
8
+ <div style="margin-bottom:1em; position:relative;"
9
+ data-controller="share-user-search"
10
+ data-share-user-search-creative-id-value="<%= (@parent_creative || @creative).id %>"
11
+ data-share-user-search-scope-value="contacts">
12
+ <label for="share-user-email"><%= t('collavre.creatives.index.user_email') %></label>
13
+ <input type="email" name="user_email" id="share-user-email"
14
+ data-share-invite-target="email"
15
+ data-share-user-search-target="input"
16
+ data-action="blur->share-invite#check input->share-user-search#input focus->share-user-search#focus keydown->share-user-search#handleKey"
17
+ autocomplete="off" />
18
+ <%= render Collavre::UserMentionMenuComponent.new(menu_id: 'share-user-suggestions') %>
19
+ </div>
20
+ <div style="margin-bottom:1em;">
21
+ <select name="permission" id="share-permission">
22
+ <option value="no_access"><%= t('collavre.creatives.index.permission_no_access') %></option>
23
+ <option value="read"><%= t('collavre.creatives.index.permission_read') %></option>
24
+ <option value="feedback"><%= t('collavre.creatives.index.permission_feedback') %></option>
25
+ <option value="write"><%= t('collavre.creatives.index.permission_write') %></option>
26
+ <option value="admin"><%= t('collavre.creatives.index.permission_admin') %></option>
27
+ </select>
28
+ </div>
29
+ <button type="submit" class="btn btn-primary" data-share-invite-target="submit" data-share="<%= t('collavre.creatives.index.share') %>" data-invite="<%= t('collavre.creatives.index.invite') %>"><%= t('collavre.creatives.index.share') %></button>
30
+ <button type="button" id="creative-invite-link" class="btn btn-secondary" data-creative-id="<%= (@parent_creative || @creative).id %>" data-no-access-message="<%= t('collavre.creatives.index.invite_link_no_access') %>" data-copied-template="<%= t('collavre.creatives.index.invite_link_copied_permission', permission: '__PERMISSION__') %>"><%= t('collavre.creatives.index.invite_link') %></button>
31
+ </form>
32
+ <% if @shared_list.any? %>
33
+ <div style="margin-top:1em;">
34
+ <strong><%= t('collavre.creatives.index.shared_with') %>:</strong>
35
+ <ul class="share-grid">
36
+ <% @shared_list.each do |share| %>
37
+ <li>
38
+ <span>
39
+ <%= render Collavre::AvatarComponent.new(user: share.user, size: 20, classes: 'avatar share-avatar') %>
40
+ </span>
41
+ <span><%= share.user&.display_name || (share.user_id.nil? ? t('collavre.creatives.index.public_share') : t('collavre.creatives.index.unknown_user')) %></span>
42
+ <span><%= t("collavre.creatives.index.permission_#{share.permission}") %></span>
43
+ <span>
44
+ <%= button_to '×', collavre.creative_creative_share_path(@parent_creative || @creative, share), method: :delete, form: { data: { turbo_confirm: t('collavre.creatives.index.are_you_sure_delete_share') } }, class: 'delete-share-btn' %>
45
+ </span>
46
+ </li>
47
+ <% end %>
48
+ </ul>
49
+ </div>
50
+ <% end %>
51
+ </div>
52
+ </div>
@@ -33,9 +33,6 @@
33
33
  align: :left,
34
34
  button_classes: 'desktop-only'
35
35
  ) do %>
36
- <%# Legacy hardcoded integrations (to be migrated to IntegrationRegistry) %>
37
- <button type="button" class="popup-menu-item" data-controller="click-target" data-click-target-id-value="github-integration-btn" data-action="click->click-target#trigger">Github</button>
38
- <%# Dynamically registered integrations %>
39
36
  <%= render 'integrations_menu', creative: current_creative %>
40
37
  <% end %>
41
38
  <% end %>
@@ -63,12 +60,14 @@
63
60
  <button id="export-markdown-btn" class="export-btn desktop-only" data-parent-creative-id="<%= @parent_creative&.id %>" data-error="<%= t('collavre.creatives.index.export_failed') %>"><%= t('.export_markdown') %></button>
64
61
  <%= render 'mobile_actions_menu', current_creative: current_creative, can_manage_integrations: can_manage_integrations %>
65
62
  </div>
63
+
64
+ <% if (@parent_creative || @creative) && (@parent_creative || @creative).has_permission?(Current.user, :write) %>
65
+ <%= render 'share_modal' %>
66
+ <% end %>
67
+
66
68
  <%= render 'import_upload_zone' %>
67
69
 
68
70
  <% if can_manage_integrations %>
69
- <%# Legacy hardcoded integration triggers %>
70
- <button id="github-integration-btn" data-creative-id="<%= current_creative&.id %>" style="display:none;"></button>
71
- <%# Dynamically registered integration triggers %>
72
71
  <%= render 'integration_triggers', creative: current_creative %>
73
72
  <% end %>
74
73
 
@@ -202,8 +201,6 @@
202
201
 
203
202
  <%= render 'set_plan_modal' %>
204
203
  <% if can_manage_integrations %>
205
- <%# Legacy hardcoded integration modals %>
206
- <%= render 'collavre_github/integrations/modal', creative: current_creative %>
207
204
  <%# Dynamically registered integration modals %>
208
205
  <%= render 'integration_modals', creative: current_creative %>
209
206
  <% end %>
@@ -4,14 +4,14 @@
4
4
 
5
5
  <div id="inbox-panel" class="slide-panel">
6
6
  <div class="inbox-panel-header">
7
- <button id="close-inbox" type="button">&times;</button>
7
+ <button id="close-inbox" class="popup-close-btn" type="button">&times;</button>
8
8
  <label><input type="checkbox" id="toggle-inbox-read"> <%= t('collavre.inbox.show_read') %></label>
9
9
  </div>
10
10
  <div id="inbox-list" data-loading-text="<%= t('app.loading') %>" data-empty-text="<%= t('collavre.inbox.no_messages') %>"></div>
11
11
  </div>
12
12
 
13
13
  <div id="creative-guide-popover" style="display:none; position:fixed; top:60px; left:50%; transform:translateX(-50%); background:white; border:1px solid var(--color-border); box-shadow:0 2px 8px rgba(0,0,0,0.12); padding:1.2em; max-width:400px; z-index:1000; border-radius:8px;">
14
- <span style="float:right; cursor:pointer; font-weight:bold;" id="close-creative-guide">&times;</span>
14
+ <button type="button" id="close-creative-guide" class="popup-close-btn">&times;</button>
15
15
  <div style="margin-top:0.5em; color:#444; font-size:1em; line-height:1.5;">
16
16
  <%= t('app.creative_guide') %>
17
17
  </div>
@@ -1,7 +1,7 @@
1
1
  <div style="max-width: 960px; margin: 0 auto; padding: 1rem;">
2
2
  <h1><%= t('collavre.themes.manage_themes') %></h1>
3
3
 
4
- <div class="card" style="margin-bottom: 2rem; padding: 1.5rem; background: var(--color-section-bg); border: 1px solid var(--color-border); border-radius: 8px;">
4
+ <div class="card" style="margin-bottom: 2rem; padding: 1.5rem; background: var(--surface-section); border: 1px solid var(--border-color); border-radius: 8px;">
5
5
  <h3><%= t('collavre.themes.create_new') %></h3>
6
6
  <%= form_with url: collavre.user_themes_path, method: :post, local: true do |f| %>
7
7
  <div class="stacked-form-control">
@@ -38,29 +38,27 @@
38
38
  <td>
39
39
  <div style="display: flex; gap: 4px;">
40
40
  <%
41
- preview_vars = theme.variables.slice("--color-bg", "--color-text", "--color-btn-bg", "--color-link")
41
+ preview_vars = theme.variables.slice("--surface-bg", "--text-primary", "--surface-btn", "--color-link")
42
42
  %>
43
43
  <% preview_vars.each do |k, v| %>
44
- <div title="<%= k %>: <%= v %>" style="width: 20px; height: 20px; background-color: <%= v %>; border: 1px solid var(--color-border); border-radius: 4px;"></div>
44
+ <div title="<%= k %>: <%= v %>" style="width: 20px; height: 20px; background-color: <%= v %>; border: 1px solid var(--border-color); border-radius: 4px;"></div>
45
45
  <% end %>
46
46
  </div>
47
47
  </td>
48
48
  <td><%= l theme.created_at, format: :short %></td>
49
49
  <td>
50
50
  <div style="display: flex; gap: 0.5rem;">
51
- <% if Current.user.theme == theme.id.to_s %>
52
- <span class="badge badge-success"><%= t('collavre.themes.active') %></span>
53
- <% else %>
54
- <%= button_to t('collavre.themes.apply'), collavre.apply_user_theme_path(theme), method: :post, class: "btn-sm" %>
51
+ <% unless Current.user.theme == theme.id.to_s %>
52
+ <%= button_to t('collavre.themes.apply'), collavre.apply_user_theme_path(theme), method: :post, class: "btn btn-xs" %>
55
53
  <% end %>
56
- <%= button_to t('collavre.themes.delete'), collavre.user_theme_path(theme), method: :delete, class: "btn-sm btn-danger", form_class: "inline-block", onclick: "return confirm('#{t('collavre.themes.confirm_delete')}')" %>
54
+ <%= button_to t('collavre.themes.delete'), collavre.user_theme_path(theme), method: :delete, class: "btn btn-xs btn-danger", form_class: "inline-block", onclick: "return confirm('#{t('collavre.themes.confirm_delete')}')" %>
57
55
  </div>
58
56
  </td>
59
57
  </tr>
60
58
  <% end %>
61
59
  <% if @themes.empty? %>
62
60
  <tr>
63
- <td colspan="4" style="text-align: center; color: var(--color-muted); padding: 2rem;">
61
+ <td colspan="4" style="text-align: center; color: var(--text-muted); padding: 2rem;">
64
62
  <p><%= t('collavre.themes.no_themes') %></p>
65
63
  </td>
66
64
  </tr>
@@ -44,11 +44,12 @@
44
44
  <td class="text-right">
45
45
  <% if contact_user.ai_user? && contact_user.created_by_id == Current.user.id %>
46
46
  <%= link_to t('collavre.users.edit_ai.link'), collavre.edit_ai_user_path(contact_user), class: "btn btn-sm btn-secondary mr-1" %>
47
+ <%= link_to t('collavre.users.copy_ai.link'), collavre.new_ai_users_path(copy_from: contact_user.id), class: "btn btn-sm btn-secondary mr-1" %>
47
48
  <%= button_to t('collavre.users.destroy.delete_ai_user'),
48
49
  collavre.user_path(contact_user),
49
50
  method: :delete,
50
51
  form: { data: { turbo_confirm: t('collavre.users.destroy.confirm_ai') } },
51
- class: "danger-link" %>
52
+ class: "btn btn-xs btn-danger" %>
52
53
  <% end %>
53
54
  </td>
54
55
  <% end %>
@@ -1,3 +1,4 @@
1
+ <%= stylesheet_link_tag 'collavre/mention_menu', media: 'all' %>
1
2
  <h1 class="text-center" style="margin-top: 0;"><%= t('collavre.users.edit_ai.title') %></h1>
2
3
 
3
4
  <%= form_with model: @user, url: collavre.update_ai_user_path(@user), method: :patch, class: "stacked-form" do |form| %>
@@ -82,6 +83,12 @@
82
83
  <% end %>
83
84
  </div>
84
85
 
86
+ <div class="form-group">
87
+ <%= form.label :agent_conf, t('collavre.users.edit_ai.agent_conf_label', default: 'Agent Configuration (YAML)') %>
88
+ <%= form.text_area :agent_conf, class: "stacked-form-control", rows: 6, placeholder: "context:\n chat_history: 50\n chat_history_size: 100000", style: "font-family: monospace; font-size: 0.85em;" %>
89
+ <small class="form-text text-muted"><%= t('collavre.users.edit_ai.agent_conf_help', default: 'YAML configuration for agent behavior. chat_history: max previous messages, chat_history_size: max total characters.') %></small>
90
+ </div>
91
+
85
92
  <div class="form-group">
86
93
  <div class="form-check">
87
94
  <%= form.check_box :searchable, { class: "form-check-input" }, true, false %>
@@ -59,16 +59,31 @@
59
59
  <% if user.system_admin? %>
60
60
  <%= button_to t('collavre.users.make_normal_user'),
61
61
  collavre.revoke_system_admin_user_path(user),
62
- method: :patch %>
62
+ method: :patch,
63
+ class: 'btn btn-xs btn-secondary' %>
63
64
  <% else %>
64
65
  <%= button_to t('collavre.users.make_system_admin'),
65
66
  collavre.grant_system_admin_user_path(user),
66
67
  method: :patch,
68
+ class: 'btn btn-xs btn-success',
67
69
  form: { data: { turbo_confirm: t('collavre.users.confirm_grant_system_admin') } } %>
68
70
  <% end %>
71
+ <% if user.locked? %>
72
+ <%= button_to t('collavre.users.unlock.button'),
73
+ collavre.unlock_user_path(user),
74
+ method: :patch,
75
+ class: 'btn btn-xs btn-secondary' %>
76
+ <% else %>
77
+ <%= button_to t('collavre.users.lock.button'),
78
+ collavre.lock_user_path(user),
79
+ method: :patch,
80
+ class: 'btn btn-xs btn-danger',
81
+ form: { data: { turbo_confirm: t('collavre.users.lock.confirm') } } %>
82
+ <% end %>
69
83
  <%= button_to t('collavre.users.delete'),
70
84
  collavre.user_path(user),
71
85
  method: :delete,
86
+ class: 'btn btn-xs btn-danger',
72
87
  form: { data: { turbo_confirm: t('collavre.users.confirm_destroy') } } %>
73
88
  <% else %>
74
89
  <span class="text-muted"><%= t('collavre.users.table.current_user') %></span>
@@ -1,24 +1,25 @@
1
+ <%= stylesheet_link_tag 'collavre/mention_menu', media: 'all' %>
1
2
  <h1 class="text-center" style="margin-top: 0;"><%= t('collavre.users.new_ai.title') %></h1>
2
3
 
3
4
  <%= form_with url: collavre.create_ai_users_path, class: "stacked-form" do |form| %>
4
5
  <div class="form-group">
5
6
  <%= form.label :ai_id, t('collavre.users.new_ai.id_label') %>
6
- <%= form.text_field :ai_id, required: true, class: "stacked-form-control", placeholder: "e.g. my_bot" %>
7
+ <%= form.text_field :ai_id, required: true, class: "stacked-form-control", placeholder: "e.g. my_bot", value: @copy_source ? "#{@copy_source.email.split('@').first}_copy" : nil %>
7
8
  </div>
8
9
 
9
10
  <div class="form-group">
10
11
  <%= form.label :name, t('collavre.users.new_ai.name_label') %>
11
- <%= form.text_field :name, required: true, class: "stacked-form-control", placeholder: "e.g. My Bot" %>
12
+ <%= form.text_field :name, required: true, class: "stacked-form-control", placeholder: "e.g. My Bot", value: @copy_name %>
12
13
  </div>
13
14
 
14
15
  <div class="form-group">
15
16
  <%= form.label :system_prompt, t('collavre.users.new_ai.system_prompt_label') %>
16
- <%= form.text_area :system_prompt, required: true, class: "stacked-form-control", rows: 5, value: AiClient::SYSTEM_INSTRUCTIONS %>
17
+ <%= form.text_area :system_prompt, required: true, class: "stacked-form-control", rows: 5, value: @copy_source&.system_prompt || AiClient::SYSTEM_INSTRUCTIONS %>
17
18
  </div>
18
19
 
19
20
  <div class="form-group">
20
21
  <%= form.label :routing_expression, "Routing Expression (Liquid)" %>
21
- <%= form.text_area :routing_expression, class: "stacked-form-control", rows: 2, placeholder: "chat.mentioned_user.id == agent.id" %>
22
+ <%= form.text_area :routing_expression, class: "stacked-form-control", rows: 2, placeholder: "chat.mentioned_user.id == agent.id", value: @copy_source&.routing_expression %>
22
23
  <small class="form-text text-muted">Liquid expression to determine if this agent should handle an event. Example: <code>chat.mentioned_user.id == agent.id</code></small>
23
24
  </div>
24
25
 
@@ -27,7 +28,7 @@
27
28
  <%= form.select :llm_vendor, options_for_select([
28
29
  ["Google (Gemini)", "google"],
29
30
  ["OpenClaw", "openclaw"]
30
- ], "google"), {}, class: "stacked-form-control" %>
31
+ ], @copy_source&.llm_vendor || "google"), {}, class: "stacked-form-control" %>
31
32
  <small class="form-text text-muted"><%= t('collavre.users.new_ai.vendor_help', default: 'Select the AI provider for this agent') %></small>
32
33
  </div>
33
34
 
@@ -41,6 +42,7 @@
41
42
  required: true,
42
43
  class: "stacked-form-control",
43
44
  autocomplete: "off",
45
+ value: @copy_source&.llm_model,
44
46
  data: {
45
47
  llm_model_target: "input",
46
48
  action: "input->llm-model#search focus->llm-model#focus blur->llm-model#blur keydown->llm-model#handleKeydown"
@@ -57,7 +59,7 @@
57
59
 
58
60
  <div class="form-group">
59
61
  <%= form.label :gateway_url, t('collavre.users.new_ai.gateway_url_label', default: 'Gateway URL') %>
60
- <%= form.text_field :gateway_url, class: "stacked-form-control", placeholder: "http://localhost:18789" %>
62
+ <%= form.text_field :gateway_url, class: "stacked-form-control", placeholder: "http://localhost:18789", value: @copy_source&.gateway_url %>
61
63
  <small class="form-text text-muted"><%= t('collavre.users.new_ai.gateway_url_help', default: 'Required for OpenClaw. The URL of your OpenClaw gateway.') %></small>
62
64
  </div>
63
65
 
@@ -69,7 +71,7 @@
69
71
  <div class="tools-selection">
70
72
  <% @available_tools.each do |tool| %>
71
73
  <div class="form-check mb-2">
72
- <%= check_box_tag "tools[]", tool[:name], false, id: "tool_#{tool[:name]}", class: "form-check-input" %>
74
+ <%= check_box_tag "tools[]", tool[:name], @copy_source&.tools&.include?(tool[:name]) || false, id: "tool_#{tool[:name]}", class: "form-check-input" %>
73
75
  <label class="form-check-label" for="tool_<%= tool[:name] %>">
74
76
  <strong><%= tool[:name] %></strong>
75
77
  <br>
@@ -87,9 +89,17 @@
87
89
  <% end %>
88
90
  </div>
89
91
 
92
+ <% if Collavre::User.column_names.include?("agent_conf") %>
93
+ <div class="form-group">
94
+ <%= form.label :agent_conf, t('collavre.users.edit_ai.agent_conf_label', default: 'Agent Configuration (YAML)') %>
95
+ <%= form.text_area :agent_conf, class: "stacked-form-control", rows: 6, placeholder: "context:\n chat_history: 50\n chat_history_size: 100000", style: "font-family: monospace; font-size: 0.85em;", value: @copy_source&.agent_conf %>
96
+ <small class="form-text text-muted"><%= t('collavre.users.edit_ai.agent_conf_help', default: 'YAML configuration for agent behavior.') %></small>
97
+ </div>
98
+ <% end %>
99
+
90
100
  <div class="form-group">
91
101
  <div class="form-check">
92
- <%= form.check_box :searchable, { class: "form-check-input" }, true, false %>
102
+ <%= form.check_box :searchable, { class: "form-check-input", checked: @copy_source&.searchable }, true, false %>
93
103
  <%= form.label :searchable, t('collavre.users.new_ai.searchable_label'), class: "form-check-label" %>
94
104
  </div>
95
105
  <small class="form-text text-muted"><%= t('collavre.users.new_ai.searchable_help') %></small>
@@ -25,7 +25,7 @@
25
25
  <td><%= credential.nickname %></td>
26
26
  <td><%= l(credential.created_at.to_date) %></td>
27
27
  <td>
28
- <%= button_to t('collavre.users.webauthn.delete'), main_app.webauthn_credential_path(credential), method: :delete, class: "btn-danger btn-sm", data: { action: "click->webauthn#delete" } %>
28
+ <%= button_to t('collavre.users.webauthn.delete'), main_app.webauthn_credential_path(credential), method: :delete, class: "btn btn-xs btn-danger", data: { action: "click->webauthn#delete" } %>
29
29
  </td>
30
30
  </tr>
31
31
  <% end %>
@@ -120,7 +120,7 @@
120
120
  <%= link_to t('doorkeeper.my_applications'), main_app.oauth_applications_path %>
121
121
  <% if @user.system_admin? %>
122
122
  <br>
123
- <%= link_to t('admin.title'), main_app.admin_path %>
123
+ <%= link_to t('admin.title'), collavre.admin_settings_path %>
124
124
  <% end %>
125
125
  </div>
126
126
 
@@ -7,6 +7,7 @@
7
7
  <%= csp_meta_tag %>
8
8
  <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
9
9
  <%= stylesheet_link_tag "collavre/creatives" %>
10
+ <%= stylesheet_link_tag "collavre/design_tokens" %>
10
11
  <%= stylesheet_link_tag "collavre/dark_mode" %>
11
12
  <%= stylesheet_link_tag 'collavre/slide_view', media: 'all' %>
12
13
  <%= javascript_include_tag 'actioncable', defer: true %>
@@ -18,12 +19,18 @@
18
19
  <% custom_theme.variables.each do |key, value| %>
19
20
  <%= key %>: <%= value %> !important;
20
21
  <% end %>
22
+ <% legacy_alias_declarations(custom_theme.variables).each do |key, value| %>
23
+ <%= key %>: <%= value %> !important;
24
+ <% end %>
25
+ <% if custom_theme.dark? %>
26
+ --color-scheme: dark !important;
27
+ <% end %>
21
28
  }
22
29
  </style>
23
30
  <% end %>
24
31
  <% end %>
25
32
  </head>
26
- <body class="<%= 'dark-mode' if Current.user&.theme == 'dark' %><%= ' light-mode' if Current.user&.theme == 'light' %>">
33
+ <body class="<%= body_theme_class %>">
27
34
  <%= yield %>
28
35
  </body>
29
36
  </html>
@@ -0,0 +1,88 @@
1
+ en:
2
+ admin:
3
+ title: "Admin Settings"
4
+ tabs:
5
+ system: "System"
6
+ uiux: "UI / UX"
7
+ users: "Users"
8
+ orchestration: "AI Orchestration"
9
+ settings:
10
+ help_link: "Help Menu Link URL"
11
+ help_link_hint: "If set, the \"?\" menu in the top navigation will link to this URL."
12
+ mcp_tool_approval: "MCP Tool Approval"
13
+ mcp_tool_approval_hint: "Require system administrator approval for new MCP tools."
14
+ creatives_login_required: "Require Login for Creatives"
15
+ creatives_login_required_hint: "If checked, users must be logged in to view /creatives."
16
+ home_page_path: "Home Page Path"
17
+ home_page_path_hint: "Path to render when accessing '/'. The URL stays as '/' but shows content from the specified path. Leave empty to use default (/creatives). Examples: /creatives, /user"
18
+ home_page_path_invalid_url: "Home page path cannot be a full URL. Please enter a path like /creatives or /user."
19
+ home_page_path_not_routable: "Home page path '%{path}' is not a valid route. Please enter an existing path."
20
+ home_page_path_not_html: "Home page path '%{path}' does not serve HTML content. Please enter a path to an HTML page."
21
+ account_lockout: "Account Lockout"
22
+ max_login_attempts: "Max Login Attempts"
23
+ max_login_attempts_hint: "Number of failed login attempts before account is locked."
24
+ lockout_duration_minutes: "Lockout Duration (minutes)"
25
+ lockout_duration_minutes_hint: "How long the account remains locked after exceeding max attempts."
26
+ password_policy: "Password Policy"
27
+ password_min_length: "Minimum Password Length"
28
+ password_min_length_hint: "Minimum number of characters required for passwords."
29
+ session_timeout: "Session Timeout"
30
+ session_timeout_minutes: "Session Timeout (minutes)"
31
+ session_timeout_minutes_hint: "Automatically log out users after this period of inactivity. Set to 0 to disable."
32
+ rate_limiting: "Rate Limiting"
33
+ password_reset_rate: "Password Reset"
34
+ password_reset_rate_hint: "Limit password reset requests per IP address."
35
+ api_rate: "API Requests"
36
+ api_rate_hint: "Limit API requests per user (or IP for unauthenticated requests)."
37
+ rate_limit_requests: "Max Requests"
38
+ rate_limit_period_minutes: "Period (minutes)"
39
+ default_themes: "Default Themes"
40
+ default_themes_hint: "Set custom themes for users who haven't configured their own. Each setting applies based on the user's OS appearance mode."
41
+ default_light_theme: "When OS is Light Mode"
42
+ default_light_theme_hint: "Theme applied when the user's OS is in light mode."
43
+ default_dark_theme: "When OS is Dark Mode"
44
+ default_dark_theme_hint: "Theme applied when the user's OS is in dark mode."
45
+ theme_system_default: "System Default"
46
+ updated: "Settings updated successfully."
47
+ save: "Save Settings"
48
+ orchestration:
49
+ title: "AI Agent Orchestration"
50
+ description: "Configure how AI agents are selected and scheduled to respond. Edit the YAML below to customize policies."
51
+ policies_yaml: "Policies (YAML)"
52
+ yaml_hint: "Define arbitration and scheduling policies. Global policies apply everywhere; overrides apply to specific scopes."
53
+ reference: "Quick Reference"
54
+ arbitration_strategies: "Arbitration Strategies"
55
+ strategy_all: "All matched agents respond"
56
+ strategy_primary_first: "Primary agent responds first, others only if unavailable"
57
+ strategy_round_robin: "Rotate between agents for each message"
58
+ strategy_bid: "Select agent based on relevance score"
59
+ scheduling_options: "Scheduling Options"
60
+ opt_max_concurrent: "Maximum simultaneous jobs per agent"
61
+ opt_daily_token: "Daily token usage limit"
62
+ opt_rate_limit: "Requests allowed per minute"
63
+ opt_backoff: "Delay strategy when busy (immediate/linear/exponential)"
64
+ opt_topic_max_concurrent: "Maximum simultaneous jobs per topic (default: 1)"
65
+ collaboration_options: "Collaboration Options"
66
+ opt_a2a_focus: "Custom A2A focus instruction (nil = locale default)"
67
+ opt_a2a_completion: "Custom A2A completion instruction (nil = locale default)"
68
+ opt_a2a_followup: "Custom A2A follow-up instruction (nil = locale default)"
69
+ opt_mention_rule: "Custom mention rule (nil = locale default)"
70
+ opt_confidence_rule: "Custom confidence rule (nil = locale default)"
71
+ opt_escalation_rule: "Custom escalation rule (nil = locale default)"
72
+ opt_review_rule: "Custom review rule (nil = locale default)"
73
+ scope_types: "Override Scopes"
74
+ scope_creative: "Apply to a specific workspace"
75
+ scope_topic: "Apply to a specific conversation"
76
+ scope_user: "Apply to a specific AI agent"
77
+ save: "Save Policies"
78
+ updated: "Orchestration policies updated successfully."
79
+ yaml_syntax_error: "YAML syntax error: %{message}"
80
+ invalid_format: "Invalid format: expected a YAML object with arbitration/scheduling/collaboration keys."
81
+ unknown_policy_type: "Unknown policy type: %{type}. Use 'arbitration', 'scheduling', or 'collaboration'."
82
+ invalid_policy_structure: "Invalid structure for '%{type}': expected an object with 'global' and/or 'overrides'."
83
+ invalid_global_config: "Invalid global config for '%{type}': expected an object."
84
+ invalid_overrides: "Invalid overrides for '%{type}': expected an array."
85
+ invalid_override_format: "Invalid override at '%{type}' index %{index}: expected an object."
86
+ invalid_scope_type: "Invalid scope_type at '%{type}' index %{index}: '%{scope_type}'. Use Creative, Topic, or User."
87
+ invalid_scope_id: "Invalid scope_id at '%{type}' index %{index}: must be a positive integer."
88
+ invalid_override_config: "Invalid config at '%{type}' index %{index}: expected an object."
@@ -0,0 +1,88 @@
1
+ ko:
2
+ admin:
3
+ title: "관리자 설정"
4
+ tabs:
5
+ system: "시스템"
6
+ uiux: "UI / UX"
7
+ users: "사용자"
8
+ orchestration: "AI 오케스트레이션"
9
+ settings:
10
+ help_link: "도움말 메뉴 링크 URL"
11
+ help_link_hint: "설정 시 상단 내비게이션의 \"?\" 메뉴가 이 URL로 연결됩니다."
12
+ mcp_tool_approval: "MCP 도구 승인"
13
+ mcp_tool_approval_hint: "새 MCP 도구 등록 시 시스템 관리자의 승인이 필요합니다."
14
+ creatives_login_required: "Creatives 로그인 필요"
15
+ creatives_login_required_hint: "체크 시, 로그인을 한 사용자만 /creatives 에 접근할 수 있습니다."
16
+ home_page_path: "홈 페이지 경로"
17
+ home_page_path_hint: "'/' 접속 시 표시할 경로입니다. URL은 '/'로 유지되며 지정된 경로의 콘텐츠가 표시됩니다. 비워두면 기본값(/creatives)을 사용합니다. 예: /creatives, /user"
18
+ home_page_path_invalid_url: "홈 페이지 경로는 전체 URL이 될 수 없습니다. /creatives 또는 /user와 같은 경로를 입력하세요."
19
+ home_page_path_not_routable: "홈 페이지 경로 '%{path}'는 유효한 라우트가 아닙니다. 존재하는 경로를 입력하세요."
20
+ home_page_path_not_html: "홈 페이지 경로 '%{path}'는 HTML 콘텐츠를 제공하지 않습니다. HTML 페이지 경로를 입력하세요."
21
+ account_lockout: "계정 잠금"
22
+ max_login_attempts: "최대 로그인 시도 횟수"
23
+ max_login_attempts_hint: "계정이 잠기기 전까지 허용되는 로그인 실패 횟수입니다."
24
+ lockout_duration_minutes: "잠금 유지 시간 (분)"
25
+ lockout_duration_minutes_hint: "최대 시도 횟수 초과 후 계정이 잠긴 상태로 유지되는 시간입니다."
26
+ password_policy: "비밀번호 정책"
27
+ password_min_length: "최소 비밀번호 길이"
28
+ password_min_length_hint: "비밀번호에 필요한 최소 문자 수입니다."
29
+ session_timeout: "세션 타임아웃"
30
+ session_timeout_minutes: "세션 타임아웃 (분)"
31
+ session_timeout_minutes_hint: "지정된 시간 동안 활동이 없으면 자동 로그아웃됩니다. 0으로 설정하면 비활성화됩니다."
32
+ rate_limiting: "요청 제한"
33
+ password_reset_rate: "비밀번호 재설정"
34
+ password_reset_rate_hint: "IP 주소당 비밀번호 재설정 요청을 제한합니다."
35
+ api_rate: "API 요청"
36
+ api_rate_hint: "사용자당 API 요청을 제한합니다 (비인증 요청은 IP 기준)."
37
+ rate_limit_requests: "최대 요청 수"
38
+ rate_limit_period_minutes: "기간 (분)"
39
+ default_themes: "기본 테마"
40
+ default_themes_hint: "개인 테마를 설정하지 않은 사용자에게 적용됩니다. 각 설정은 사용자의 OS 외관 모드에 따라 적용됩니다."
41
+ default_light_theme: "OS 라이트 모드일 때"
42
+ default_light_theme_hint: "사용자의 OS가 라이트 모드일 때 적용되는 테마입니다."
43
+ default_dark_theme: "OS 다크 모드일 때"
44
+ default_dark_theme_hint: "사용자의 OS가 다크 모드일 때 적용되는 테마입니다."
45
+ theme_system_default: "시스템 기본"
46
+ updated: "설정이 성공적으로 업데이트되었습니다."
47
+ save: "설정 저장"
48
+ orchestration:
49
+ title: "AI 에이전트 오케스트레이션"
50
+ description: "AI 에이전트가 응답하도록 선택되고 스케줄링되는 방식을 설정합니다. 아래 YAML을 수정하여 정책을 커스터마이즈하세요."
51
+ policies_yaml: "정책 (YAML)"
52
+ yaml_hint: "중재(arbitration) 및 스케줄링 정책을 정의합니다. 전역 정책은 모든 곳에 적용되고, 오버라이드는 특정 범위에 적용됩니다."
53
+ reference: "빠른 참조"
54
+ arbitration_strategies: "중재 전략"
55
+ strategy_all: "매칭된 모든 에이전트가 응답"
56
+ strategy_primary_first: "기본 에이전트가 먼저 응답, 불가능할 때만 다른 에이전트"
57
+ strategy_round_robin: "메시지마다 에이전트를 순환"
58
+ strategy_bid: "관련성 점수에 따라 에이전트 선택"
59
+ scheduling_options: "스케줄링 옵션"
60
+ opt_max_concurrent: "에이전트당 최대 동시 작업 수"
61
+ opt_daily_token: "일일 토큰 사용량 한도"
62
+ opt_rate_limit: "분당 허용 요청 수"
63
+ opt_backoff: "바쁠 때 지연 전략 (immediate/linear/exponential)"
64
+ opt_topic_max_concurrent: "토픽당 최대 동시 작업 수 (기본값: 1)"
65
+ collaboration_options: "협업 옵션"
66
+ opt_a2a_focus: "A2A 포커스 지시문 커스텀 (nil = 로케일 기본값)"
67
+ opt_a2a_completion: "A2A 완료 지시문 커스텀 (nil = 로케일 기본값)"
68
+ opt_a2a_followup: "A2A 후속 지시문 커스텀 (nil = 로케일 기본값)"
69
+ opt_mention_rule: "멘션 규칙 커스텀 (nil = 로케일 기본값)"
70
+ opt_confidence_rule: "확신도 규칙 커스텀 (nil = 로케일 기본값)"
71
+ opt_escalation_rule: "에스컬레이션 규칙 커스텀 (nil = 로케일 기본값)"
72
+ opt_review_rule: "리뷰 규칙 커스텀 (nil = 로케일 기본값)"
73
+ scope_types: "오버라이드 범위"
74
+ scope_creative: "특정 워크스페이스에 적용"
75
+ scope_topic: "특정 대화에 적용"
76
+ scope_user: "특정 AI 에이전트에 적용"
77
+ save: "정책 저장"
78
+ updated: "오케스트레이션 정책이 성공적으로 업데이트되었습니다."
79
+ yaml_syntax_error: "YAML 문법 오류: %{message}"
80
+ invalid_format: "잘못된 형식: arbitration/scheduling/collaboration 키를 가진 YAML 객체가 필요합니다."
81
+ unknown_policy_type: "알 수 없는 정책 유형: %{type}. 'arbitration', 'scheduling', 또는 'collaboration'을 사용하세요."
82
+ invalid_policy_structure: "'%{type}'의 구조가 잘못되었습니다: 'global' 및/또는 'overrides'를 가진 객체가 필요합니다."
83
+ invalid_global_config: "'%{type}'의 전역 설정이 잘못되었습니다: 객체가 필요합니다."
84
+ invalid_overrides: "'%{type}'의 오버라이드가 잘못되었습니다: 배열이 필요합니다."
85
+ invalid_override_format: "'%{type}' 인덱스 %{index}의 오버라이드가 잘못되었습니다: 객체가 필요합니다."
86
+ invalid_scope_type: "'%{type}' 인덱스 %{index}의 scope_type이 잘못되었습니다: '%{scope_type}'. Creative, Topic, 또는 User를 사용하세요."
87
+ invalid_scope_id: "'%{type}' 인덱스 %{index}의 scope_id가 잘못되었습니다: 양의 정수여야 합니다."
88
+ invalid_override_config: "'%{type}' 인덱스 %{index}의 config가 잘못되었습니다: 객체가 필요합니다."
@@ -21,8 +21,11 @@ en:
21
21
  request_header: "## ⚡ Agent Request"
22
22
  request_description: "This message is a request from another AI Agent (@%{sender_name}, %{sender_type})."
23
23
  focus_instruction: "- Focus on answering the request"
24
- completion_instruction: "- Clearly communicate results when done"
24
+ completion_instruction: "- When done, you MUST report results back to @%{sender_name}: via mention"
25
25
  followup_instruction: "- If you need more information, ask the requester via @mention"
26
+ human_completion_instruction: "- When done, report your results back to @%{sender_name}: via mention"
27
+ review:
28
+ context: "The user is reviewing your previous message and requesting changes. Your response will REPLACE the original message entirely. You MUST provide the complete response — never abbreviate, skip, or summarize any part (e.g., do NOT write 'items 2-5 remain the same'). Write the full content from start to finish as if the original never existed."
26
29
  collaboration:
27
30
  header: "## Available Agents for Collaboration"
28
31
  escalation_header: "### Escalation Targets (when stuck)"
@@ -32,5 +35,6 @@ en:
32
35
  rules_header: "## Collaboration Rules"
33
36
  mention_rule: "- Call other agents: @name: request"
34
37
  confidence_rule: "- Re-evaluate before responding if uncertain"
38
+ confidence_format_instruction: "- End your response with [confidence: 0-100] to indicate your certainty level (e.g. [confidence: 85])"
35
39
  escalation_rule: "- Ask escalation targets for help when stuck"
36
40
  review_rule: "- Request review from reviewers when code review is needed"
@@ -21,8 +21,11 @@ ko:
21
21
  request_header: "## ⚡ Agent 요청"
22
22
  request_description: "이 메시지는 다른 AI Agent(@%{sender_name}, %{sender_type})가 보낸 요청입니다."
23
23
  focus_instruction: "- 요청에 집중해서 답변하세요"
24
- completion_instruction: "- 완료되면 결과를 명확히 전달하세요"
24
+ completion_instruction: "- 완료되면 반드시 @%{sender_name}: 멘션으로 결과를 보고하세요"
25
25
  followup_instruction: "- 추가 정보가 필요하면 요청자에게 @멘션으로 물어보세요"
26
+ human_completion_instruction: "- 완료되면 @%{sender_name}: 멘션으로 결과를 보고하세요"
27
+ review:
28
+ context: "사용자가 당신의 이전 메시지를 리뷰하고 변경을 요청하고 있습니다. 당신의 응답이 원본 메시지를 완전히 대체됩니다. 반드시 전체 응답을 빠짐없이 작성하세요 — 절대로 일부를 생략하거나 요약하지 마세요 (예: '2~5번은 이전과 동일' 같은 표현 금지). 원본이 없다고 생각하고 처음부터 끝까지 완전한 내용을 작성하세요."
26
29
  collaboration:
27
30
  header: "## 협업 가능한 Agent"
28
31
  escalation_header: "### 에스컬레이션 대상 (문제 해결 불가 시)"
@@ -32,5 +35,6 @@ ko:
32
35
  rules_header: "## 협업 규칙"
33
36
  mention_rule: "- 다른 Agent 호출: @이름: 요청내용"
34
37
  confidence_rule: "- 확신이 낮으면 재검토 후 발화"
38
+ confidence_format_instruction: "- 응답 끝에 [confidence: 0-100] 형식으로 확신도를 명시하세요 (예: [confidence: 85])"
35
39
  escalation_rule: "- 막히면 에스컬레이션 대상에게 도움 요청"
36
40
  review_rule: "- 코드 리뷰가 필요하면 리뷰어에게 요청"
@@ -24,7 +24,6 @@ en:
24
24
  no_permission: You do not have permission to comment.
25
25
  convert_not_allowed: Only the comment owner or a creative admin can convert
26
26
  this message.
27
- gemini: Gemini
28
27
  convert_button: Convert
29
28
  convert_to_creative: Convert to Creative
30
29
  convert_confirm: This will add the comment content as a child creative of the
@@ -46,6 +45,7 @@ en:
46
45
  approve_invalid_attributes: Comment action attributes must be an object.
47
46
  approve_no_attributes: Comment action must include at least one allowed attribute.
48
47
  approve_invalid_creative: Comment action targets an invalid creative.
48
+ approve_no_write_permission: No write permission to perform this action on the creative.
49
49
  approve_invalid_progress: Comment action progress must be a number.
50
50
  approve_invalid_description: Comment action description must be text.
51
51
  approved_label: Approved
@@ -65,6 +65,8 @@ 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
69
+ replace_button: Replace
68
70
  move_no_selection: Select at least one message to move.
69
71
  move_error: Unable to move messages.
70
72
  add_participant: Add user
@@ -87,6 +89,8 @@ en:
87
89
  event_created: 'event created: [Google Calendar event](%{url})'
88
90
  event_created_local: 'event created (local only - connect Google Calendar to sync)'
89
91
  event_created_sync_failed: 'event created (Google Calendar sync failed - please reconnect your Google account)'
92
+ mcp_command:
93
+ error_running: "Error running /%{tool_name}: %{error}"
90
94
  topic_command:
91
95
  missing_name: 'Please specify a topic name in quotes: /topic "topic name"'
92
96
  created: 'Topic "%{name}" created.'