collavre 0.15.0 → 0.20.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 +4 -4
- data/app/assets/stylesheets/collavre/code_highlight.css +13 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +65 -5
- data/app/assets/stylesheets/collavre/creatives.css +5 -2
- data/app/controllers/collavre/admin/settings_controller.rb +9 -0
- data/app/controllers/collavre/attachments_controller.rb +1 -1
- data/app/controllers/collavre/comment_read_pointers_controller.rb +2 -2
- data/app/controllers/collavre/comments/snapshots_controller.rb +2 -7
- data/app/controllers/collavre/comments_controller.rb +23 -25
- data/app/controllers/collavre/creatives_controller.rb +22 -11
- data/app/controllers/collavre/emails_controller.rb +2 -0
- data/app/controllers/collavre/google_auth_controller.rb +1 -1
- data/app/controllers/collavre/inbox_items_controller.rb +1 -1
- data/app/controllers/collavre/invites_controller.rb +3 -0
- data/app/controllers/collavre/tasks_controller.rb +1 -1
- data/app/controllers/collavre/topics_controller.rb +27 -85
- data/app/controllers/concerns/collavre/comments/comment_scoping.rb +3 -0
- data/app/controllers/concerns/collavre/creative_permission_guard.rb +32 -0
- data/app/controllers/concerns/collavre/integration_setup.rb +17 -0
- data/app/helpers/collavre/application_helper.rb +30 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +7 -3
- data/app/javascript/components/creative_tree_row.js +21 -1
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +34 -0
- data/app/javascript/controllers/comment_controller.js +19 -1
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/form_controller.js +7 -4
- data/app/javascript/controllers/comments/list_controller.js +43 -4
- data/app/javascript/controllers/comments/popup_controller.js +45 -12
- data/app/javascript/controllers/comments/presence_controller.js +8 -0
- data/app/javascript/controllers/comments/topics_controller.js +50 -31
- data/app/javascript/creatives/tree_renderer.js +1 -0
- data/app/javascript/lib/__tests__/chat_history.test.js +31 -0
- data/app/javascript/lib/chat_history.js +12 -2
- data/app/javascript/lib/utils/markdown.js +32 -1
- data/app/javascript/modules/command_args_form.js +8 -0
- data/app/javascript/modules/creative_row_editor.js +12 -17
- data/app/javascript/modules/integration_wizard.js +162 -0
- data/app/jobs/collavre/compress_job.rb +1 -0
- data/app/jobs/collavre/creative_broadcast_job.rb +4 -1
- data/app/jobs/collavre/merge_comments_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_check_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_verify_job.rb +1 -0
- data/app/models/collavre/calendar_event.rb +0 -4
- data/app/models/collavre/comment/broadcastable.rb +1 -1
- data/app/models/collavre/comment.rb +17 -2
- data/app/models/collavre/comment_snapshot.rb +0 -1
- data/app/models/collavre/creative/describable.rb +10 -1
- data/app/models/collavre/creative/realtime_broadcastable.rb +17 -5
- data/app/models/collavre/creative.rb +43 -1
- data/app/models/collavre/current.rb +1 -1
- data/app/models/collavre/inbox_item.rb +0 -4
- data/app/models/collavre/system_setting.rb +10 -1
- data/app/models/collavre/task.rb +17 -8
- data/app/models/collavre/user.rb +11 -1
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +0 -8
- data/app/services/collavre/ai_agent/message_builder.rb +32 -15
- data/app/services/collavre/ai_agent/response_finalizer.rb +2 -1
- data/app/services/collavre/ai_agent/session_context_resolver.rb +50 -0
- data/app/services/collavre/ai_agent_service.rb +14 -2
- data/app/services/collavre/ai_client.rb +13 -0
- data/app/services/collavre/command_menu_service.rb +23 -3
- data/app/services/collavre/comments/command_processor.rb +1 -1
- data/app/services/collavre/comments/mcp_command.rb +27 -8
- data/app/services/collavre/comments/mcp_command_builder.rb +4 -3
- data/app/services/collavre/creatives/tree_builder.rb +3 -0
- data/app/services/collavre/google_calendar_service.rb +32 -6
- data/app/services/collavre/markdown_converter.rb +15 -20
- data/app/services/collavre/mcp_service.rb +4 -4
- data/app/services/collavre/orchestration/agent_orchestrator.rb +2 -2
- data/app/services/collavre/tools/creative_batch_service.rb +5 -1
- data/app/services/collavre/tools/cron_create_service.rb +9 -6
- data/app/services/collavre/topic_branch_service.rb +2 -2
- data/app/views/collavre/admin/settings/_system_tab.html.erb +11 -0
- data/app/views/collavre/comments/_comment.html.erb +7 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
- data/config/locales/admin.en.yml +3 -0
- data/config/locales/admin.ko.yml +3 -0
- data/config/locales/comments.en.yml +6 -1
- data/config/locales/comments.ko.yml +6 -1
- data/db/migrate/20251126040752_add_description_to_creatives.rb +1 -1
- data/db/migrate/20260415000000_create_main_topics_for_existing_creatives.rb +69 -0
- data/lib/collavre/version.rb +1 -1
- metadata +21 -4
- data/app/jobs/collavre/permission_cache_cleanup_job.rb +0 -36
- data/app/models/collavre/variation.rb +0 -5
- data/app/services/collavre/creatives/path_exporter.rb +0 -131
|
@@ -33,6 +33,8 @@ export default class extends Controller {
|
|
|
33
33
|
this.openFromUrlTimeout = null
|
|
34
34
|
this.handleCreativeClick = this.handleCreativeClick.bind(this)
|
|
35
35
|
this.handleCreativeDestroyed = this.handleCreativeDestroyed.bind(this)
|
|
36
|
+
this.handleEditingStart = this.handleEditingStart.bind(this)
|
|
37
|
+
this.handleEditingStop = this.handleEditingStop.bind(this)
|
|
36
38
|
this.handleTouchStart = this.handleTouchStart.bind(this)
|
|
37
39
|
this.handleTouchEnd = this.handleTouchEnd.bind(this)
|
|
38
40
|
this.handleResizeMove = this.handleResizeMove.bind(this)
|
|
@@ -54,6 +56,8 @@ export default class extends Controller {
|
|
|
54
56
|
|
|
55
57
|
document.addEventListener(CREATIVE_CLICK_EVENT, this.handleCreativeClick)
|
|
56
58
|
document.addEventListener(CREATIVE_DESTROYED_EVENT, this.handleCreativeDestroyed)
|
|
59
|
+
document.addEventListener('creative-editing:start', this.handleEditingStart)
|
|
60
|
+
document.addEventListener('creative-editing:stop', this.handleEditingStop)
|
|
57
61
|
this.element.addEventListener('wheel', this.handlePopupWheel, { passive: false })
|
|
58
62
|
window.addEventListener('online', this.handleOnline)
|
|
59
63
|
window.addEventListener('focus', this.handleWindowFocus)
|
|
@@ -121,6 +125,8 @@ export default class extends Controller {
|
|
|
121
125
|
this.clearPendingOpenFromUrl()
|
|
122
126
|
document.removeEventListener(CREATIVE_CLICK_EVENT, this.handleCreativeClick)
|
|
123
127
|
document.removeEventListener(CREATIVE_DESTROYED_EVENT, this.handleCreativeDestroyed)
|
|
128
|
+
document.removeEventListener('creative-editing:start', this.handleEditingStart)
|
|
129
|
+
document.removeEventListener('creative-editing:stop', this.handleEditingStop)
|
|
124
130
|
this.element.removeEventListener('wheel', this.handlePopupWheel)
|
|
125
131
|
window.removeEventListener('online', this.handleOnline)
|
|
126
132
|
window.removeEventListener('focus', this.handleWindowFocus)
|
|
@@ -202,6 +208,16 @@ export default class extends Controller {
|
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
handleEditingStart() {
|
|
212
|
+
if (this.element.style.display === 'flex' && !this.isFullscreen()) {
|
|
213
|
+
this.element.classList.add('editor-behind')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
handleEditingStop() {
|
|
218
|
+
this.element.classList.remove('editor-behind')
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
async open(button, { creativeId, highlightId } = {}) {
|
|
206
222
|
this.currentButton = button
|
|
207
223
|
const resolvedCreativeId = creativeId || button?.dataset.creativeId
|
|
@@ -378,7 +394,7 @@ export default class extends Controller {
|
|
|
378
394
|
this._releaseWakeLock()
|
|
379
395
|
|
|
380
396
|
this.element.style.display = 'none'
|
|
381
|
-
this.element.classList.remove('open')
|
|
397
|
+
this.element.classList.remove('open', 'editor-behind')
|
|
382
398
|
this.element.style.width = ''
|
|
383
399
|
this.element.style.height = ''
|
|
384
400
|
this.element.style.left = ''
|
|
@@ -623,6 +639,16 @@ export default class extends Controller {
|
|
|
623
639
|
// Enter fullscreen immediately without animation (for auto-fullscreen on page load)
|
|
624
640
|
_enterFullscreenImmediate() {
|
|
625
641
|
const el = this.element
|
|
642
|
+
|
|
643
|
+
// Save current inline styles so exit-fullscreen can restore them
|
|
644
|
+
this._savedStyles = {
|
|
645
|
+
top: el.style.top,
|
|
646
|
+
right: el.style.right,
|
|
647
|
+
left: el.style.left,
|
|
648
|
+
width: el.style.width,
|
|
649
|
+
height: el.style.height,
|
|
650
|
+
}
|
|
651
|
+
|
|
626
652
|
el.style.transition = 'none'
|
|
627
653
|
el.dataset.fullscreen = 'true'
|
|
628
654
|
document.body.classList.add('chat-fullscreen')
|
|
@@ -695,8 +721,10 @@ export default class extends Controller {
|
|
|
695
721
|
}
|
|
696
722
|
|
|
697
723
|
// Clean up inline styles after transition ends
|
|
698
|
-
|
|
699
|
-
el.removeEventListener('transitionend',
|
|
724
|
+
this._enterCleanupFn = () => {
|
|
725
|
+
el.removeEventListener('transitionend', this._enterCleanupFn)
|
|
726
|
+
this._enterCleanupTimer = null
|
|
727
|
+
this._enterCleanupFn = null
|
|
700
728
|
el.style.top = ''
|
|
701
729
|
el.style.left = ''
|
|
702
730
|
el.style.right = ''
|
|
@@ -705,11 +733,22 @@ export default class extends Controller {
|
|
|
705
733
|
el.style.height = ''
|
|
706
734
|
el.style.position = ''
|
|
707
735
|
}
|
|
708
|
-
el.addEventListener('transitionend',
|
|
736
|
+
el.addEventListener('transitionend', this._enterCleanupFn, { once: true })
|
|
709
737
|
// Fallback if transitionend doesn't fire
|
|
710
|
-
setTimeout(
|
|
738
|
+
this._enterCleanupTimer = setTimeout(this._enterCleanupFn, 300)
|
|
711
739
|
|
|
712
740
|
} else {
|
|
741
|
+
// Cancel any pending enter-fullscreen cleanup to prevent it from
|
|
742
|
+
// wiping inline styles mid-exit animation (race condition fix)
|
|
743
|
+
if (this._enterCleanupTimer) {
|
|
744
|
+
clearTimeout(this._enterCleanupTimer)
|
|
745
|
+
this._enterCleanupTimer = null
|
|
746
|
+
}
|
|
747
|
+
if (this._enterCleanupFn) {
|
|
748
|
+
el.removeEventListener('transitionend', this._enterCleanupFn)
|
|
749
|
+
this._enterCleanupFn = null
|
|
750
|
+
}
|
|
751
|
+
|
|
713
752
|
const savedStyles = this._savedStyles
|
|
714
753
|
this._savedStyles = null
|
|
715
754
|
const creativeId = el.dataset.creativeId
|
|
@@ -1062,21 +1101,15 @@ export default class extends Controller {
|
|
|
1062
1101
|
if (hasOverflowY || hasOverflowX) {
|
|
1063
1102
|
const style = getComputedStyle(el)
|
|
1064
1103
|
|
|
1065
|
-
// Check dominant axis first for better matching
|
|
1066
1104
|
if (dominantAxis === 'x' && hasOverflowX) {
|
|
1067
1105
|
const scrollableX = style.overflowX === 'auto' || style.overflowX === 'scroll'
|
|
1068
1106
|
if (scrollableX) return { element: el, axis: 'x' }
|
|
1069
1107
|
}
|
|
1070
1108
|
|
|
1071
|
-
if (hasOverflowY) {
|
|
1109
|
+
if (dominantAxis === 'y' && hasOverflowY) {
|
|
1072
1110
|
const scrollableY = style.overflowY === 'auto' || style.overflowY === 'scroll'
|
|
1073
1111
|
if (scrollableY) return { element: el, axis: 'y' }
|
|
1074
1112
|
}
|
|
1075
|
-
|
|
1076
|
-
if (dominantAxis !== 'x' && hasOverflowX) {
|
|
1077
|
-
const scrollableX = style.overflowX === 'auto' || style.overflowX === 'scroll'
|
|
1078
|
-
if (scrollableX) return { element: el, axis: 'x' }
|
|
1079
|
-
}
|
|
1080
1113
|
}
|
|
1081
1114
|
|
|
1082
1115
|
el = el.parentElement
|
|
@@ -64,6 +64,7 @@ export default class extends Controller {
|
|
|
64
64
|
this.currentPresentIds = []
|
|
65
65
|
this.typingUsers = {}
|
|
66
66
|
this.activeAgentTasks = {}
|
|
67
|
+
this.syncGlobalAgentTasks()
|
|
67
68
|
this.clearTypingTimers()
|
|
68
69
|
this.clearManualTypingMessage()
|
|
69
70
|
this.renderParticipants([])
|
|
@@ -217,6 +218,7 @@ export default class extends Controller {
|
|
|
217
218
|
delete this.typingUsers[id]
|
|
218
219
|
delete this.agentStatusTimers[id]
|
|
219
220
|
delete this.activeAgentTasks?.[id]
|
|
221
|
+
this.syncGlobalAgentTasks()
|
|
220
222
|
this.renderTypingIndicator()
|
|
221
223
|
}, AGENT_STATUS_TIMEOUT)
|
|
222
224
|
} else {
|
|
@@ -227,6 +229,7 @@ export default class extends Controller {
|
|
|
227
229
|
delete this.agentStatusTimers[id]
|
|
228
230
|
}
|
|
229
231
|
}
|
|
232
|
+
this.syncGlobalAgentTasks()
|
|
230
233
|
this.renderTypingIndicator()
|
|
231
234
|
}
|
|
232
235
|
}
|
|
@@ -380,6 +383,10 @@ export default class extends Controller {
|
|
|
380
383
|
this.typingIndicatorTarget.appendChild(text)
|
|
381
384
|
}
|
|
382
385
|
|
|
386
|
+
syncGlobalAgentTasks() {
|
|
387
|
+
// no-op: stop button is now server-rendered per comment via task_id
|
|
388
|
+
}
|
|
389
|
+
|
|
383
390
|
cancelAllAgentTasks() {
|
|
384
391
|
const ids = Object.keys(this.activeAgentTasks || {})
|
|
385
392
|
if (ids.length === 0) return false
|
|
@@ -403,6 +410,7 @@ export default class extends Controller {
|
|
|
403
410
|
clearTimeout(this.agentStatusTimers[agentId])
|
|
404
411
|
delete this.agentStatusTimers[agentId]
|
|
405
412
|
}
|
|
413
|
+
this.syncGlobalAgentTasks()
|
|
406
414
|
this.renderTypingIndicator()
|
|
407
415
|
}
|
|
408
416
|
})
|
|
@@ -14,6 +14,7 @@ export default class extends Controller {
|
|
|
14
14
|
this.canCreateTopic = false
|
|
15
15
|
this.subscribedCreativeId = null
|
|
16
16
|
this.topicsSubscription = null
|
|
17
|
+
this._loadTopicsVersion = 0
|
|
17
18
|
// Initial load if creativeId is available (e.g. from dataset if set server-side)
|
|
18
19
|
if (this.creativeId) {
|
|
19
20
|
this.loadTopics()
|
|
@@ -61,8 +62,16 @@ export default class extends Controller {
|
|
|
61
62
|
async loadTopics() {
|
|
62
63
|
if (!this.creativeId) return
|
|
63
64
|
|
|
65
|
+
const version = ++this._loadTopicsVersion
|
|
66
|
+
// Clear stale topics from previous creative to prevent name-based
|
|
67
|
+
// dedupe in handleTopicMessage from blocking valid broadcasts
|
|
68
|
+
this.topics = []
|
|
69
|
+
|
|
64
70
|
try {
|
|
65
71
|
const response = await fetch(`/creatives/${this.creativeId}/topics`)
|
|
72
|
+
// Discard stale response if a newer loadTopics() call was made
|
|
73
|
+
if (version !== this._loadTopicsVersion) return
|
|
74
|
+
|
|
66
75
|
if (response.status === 404) {
|
|
67
76
|
throw new Error(`Creative ${this.creativeId} not found`)
|
|
68
77
|
}
|
|
@@ -78,6 +87,7 @@ export default class extends Controller {
|
|
|
78
87
|
this.serverLastTopicId = data.last_topic_id ? String(data.last_topic_id) : ""
|
|
79
88
|
this.isInbox = !!data.is_inbox
|
|
80
89
|
this.systemTopicId = data.system_topic_id ? String(data.system_topic_id) : null
|
|
90
|
+
this.mainTopicId = data.main_topic_id ? String(data.main_topic_id) : null
|
|
81
91
|
|
|
82
92
|
// Migrate localStorage to server if server has no value
|
|
83
93
|
this.migrateLocalStorage()
|
|
@@ -102,10 +112,7 @@ export default class extends Controller {
|
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
// Without this, switching to a creative with no stored topic leaves the form
|
|
107
|
-
// controller with a stale topic_id from the previous creative.
|
|
108
|
-
this.selectTopic("")
|
|
115
|
+
this.selectTopic(this.mainTopicId || "")
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
renderTopics(topics, canManage = false, canCreateTopic = canManage) {
|
|
@@ -117,30 +124,39 @@ export default class extends Controller {
|
|
|
117
124
|
? 'dragover->comments--topics#handleTopicReorderDragOver dragleave->comments--topics#handleTopicReorderDragLeave drop->comments--topics#handleTopicReorderDrop'
|
|
118
125
|
: ''
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
const allMessagesLabel = this.element.dataset.topicMainText || 'All Messages'
|
|
128
|
+
|
|
129
|
+
const mainTopic = this.mainTopicId ? topics.find(t => String(t.id) === String(this.mainTopicId)) : null
|
|
130
|
+
const otherTopics = topics.filter(t => !mainTopic || String(t.id) !== String(this.mainTopicId))
|
|
131
|
+
|
|
132
|
+
let html = ''
|
|
123
133
|
|
|
124
|
-
|
|
125
|
-
// Ensure comparison handles string/number difference
|
|
134
|
+
const renderTopic = (topic) => {
|
|
126
135
|
const isActive = String(this.currentTopicId) === String(topic.id) ? 'active' : ''
|
|
127
136
|
const draggable = canManage ? 'draggable="true"' : ''
|
|
128
137
|
const agentAvatar = topic.primary_agent
|
|
129
138
|
? this.renderAgentAvatar(topic.primary_agent)
|
|
130
139
|
: ''
|
|
131
140
|
const branchIcon = topic.source_topic_id ? '<span class="topic-branch-icon" title="Branched">↗</span>' : ''
|
|
132
|
-
|
|
141
|
+
const isMainTopic = this.mainTopicId && String(topic.id) === String(this.mainTopicId)
|
|
142
|
+
let s = `<span class="topic-tag topic-drop-target ${isActive}" ${draggable}
|
|
133
143
|
data-action="click->comments--topics#select ${dropActions} ${dragActions} ${topicDropActions}"
|
|
134
144
|
data-id="${topic.id}"${topic.source_topic_id ? ` data-source-topic-id="${topic.source_topic_id}"` : ''}>
|
|
135
145
|
${agentAvatar}${branchIcon}#${topic.name}`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
html += `<button class="delete-topic-btn" data-action="click->comments--topics#deleteTopic" data-id="${topic.id}">×</button>`
|
|
146
|
+
if (canManage && !isMainTopic) {
|
|
147
|
+
s += `<button class="archive-topic-btn" data-action="click->comments--topics#archiveTopic" data-id="${topic.id}" title="Archive">${ICON_ARCHIVE}</button>`
|
|
148
|
+
s += `<button class="delete-topic-btn" data-action="click->comments--topics#deleteTopic" data-id="${topic.id}">×</button>`
|
|
140
149
|
}
|
|
150
|
+
s += `</span>`
|
|
151
|
+
return s
|
|
152
|
+
}
|
|
141
153
|
|
|
142
|
-
|
|
143
|
-
})
|
|
154
|
+
if (mainTopic) html += renderTopic(mainTopic)
|
|
155
|
+
otherTopics.forEach(topic => { html += renderTopic(topic) })
|
|
156
|
+
|
|
157
|
+
html += `<span class="topic-tag topic-drop-target topic-all-messages ${this.currentTopicId ? '' : 'active'}"
|
|
158
|
+
data-action="click->comments--topics#select ${dropActions}"
|
|
159
|
+
data-id="">📋 ${allMessagesLabel}</span>`
|
|
144
160
|
|
|
145
161
|
// Archived topics section
|
|
146
162
|
if (this.archivedTopics && this.archivedTopics.length > 0) {
|
|
@@ -191,7 +207,7 @@ export default class extends Controller {
|
|
|
191
207
|
const agentJson = event.dataTransfer.getData('application/x-agent-drop')
|
|
192
208
|
if (agentJson) {
|
|
193
209
|
const agent = JSON.parse(agentJson)
|
|
194
|
-
const targetTopicId = event.currentTarget.dataset.id
|
|
210
|
+
const targetTopicId = event.currentTarget.dataset.id || this.mainTopicId
|
|
195
211
|
if (targetTopicId) {
|
|
196
212
|
await this.setTopicPrimaryAgent(targetTopicId, agent)
|
|
197
213
|
}
|
|
@@ -205,7 +221,7 @@ export default class extends Controller {
|
|
|
205
221
|
const commentIds = JSON.parse(commentIdsJson)
|
|
206
222
|
if (!commentIds || commentIds.length === 0) return
|
|
207
223
|
|
|
208
|
-
const targetTopicId = event.currentTarget.dataset.id
|
|
224
|
+
const targetTopicId = event.currentTarget.dataset.id || this.mainTopicId
|
|
209
225
|
|
|
210
226
|
// Dispatch event for list_controller to handle the move
|
|
211
227
|
this.dispatch('move-to-topic', {
|
|
@@ -363,7 +379,7 @@ export default class extends Controller {
|
|
|
363
379
|
if (response.ok) {
|
|
364
380
|
if (String(this.currentTopicId) === String(topicId)) {
|
|
365
381
|
this.currentTopicId = "" // Switch to Main
|
|
366
|
-
this.dispatch("change", { detail: { topicId: "" } })
|
|
382
|
+
this.dispatch("change", { detail: { topicId: "", mainTopicId: this.mainTopicId } })
|
|
367
383
|
}
|
|
368
384
|
this.loadTopics()
|
|
369
385
|
} else {
|
|
@@ -390,7 +406,7 @@ export default class extends Controller {
|
|
|
390
406
|
if (response.ok) {
|
|
391
407
|
if (String(this.currentTopicId) === String(topicId)) {
|
|
392
408
|
this.currentTopicId = ""
|
|
393
|
-
this.dispatch("change", { detail: { topicId: "" } })
|
|
409
|
+
this.dispatch("change", { detail: { topicId: "", mainTopicId: this.mainTopicId } })
|
|
394
410
|
}
|
|
395
411
|
this.loadTopics()
|
|
396
412
|
} else {
|
|
@@ -501,7 +517,7 @@ export default class extends Controller {
|
|
|
501
517
|
this.clearNewMessageBadge(id)
|
|
502
518
|
}
|
|
503
519
|
// Dispatch event
|
|
504
|
-
this.dispatch("change", { detail: { topicId: id, isInbox: this.isInbox, systemTopicId: this.systemTopicId } })
|
|
520
|
+
this.dispatch("change", { detail: { topicId: id, isInbox: this.isInbox, systemTopicId: this.systemTopicId, mainTopicId: this.mainTopicId } })
|
|
505
521
|
}
|
|
506
522
|
|
|
507
523
|
showEditInput(topicEl, topicId) {
|
|
@@ -570,10 +586,9 @@ export default class extends Controller {
|
|
|
570
586
|
|
|
571
587
|
if (response.ok) {
|
|
572
588
|
const updatedTopic = await response.json()
|
|
573
|
-
// Update local topics array
|
|
574
589
|
const index = this.topics.findIndex(t => String(t.id) === String(topicId))
|
|
575
590
|
if (index !== -1) {
|
|
576
|
-
this.topics[index] = updatedTopic
|
|
591
|
+
this.topics[index] = { ...this.topics[index], ...updatedTopic }
|
|
577
592
|
}
|
|
578
593
|
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
579
594
|
this.restoreSelection()
|
|
@@ -619,7 +634,7 @@ export default class extends Controller {
|
|
|
619
634
|
// If we were viewing the moved topic, switch to Main
|
|
620
635
|
if (String(this.currentTopicId) === String(topicId)) {
|
|
621
636
|
this.currentTopicId = ""
|
|
622
|
-
this.dispatch("change", { detail: { topicId: "" } })
|
|
637
|
+
this.dispatch("change", { detail: { topicId: "", mainTopicId: this.mainTopicId } })
|
|
623
638
|
}
|
|
624
639
|
this.loadTopics()
|
|
625
640
|
}
|
|
@@ -667,7 +682,7 @@ export default class extends Controller {
|
|
|
667
682
|
await this.flushSaveLastTopic(topic.id)
|
|
668
683
|
await this.loadTopics()
|
|
669
684
|
// Dispatch change event manually since we skipped the click handler
|
|
670
|
-
this.dispatch("change", { detail: { topicId: topic.id } })
|
|
685
|
+
this.dispatch("change", { detail: { topicId: topic.id, mainTopicId: this.mainTopicId } })
|
|
671
686
|
} else {
|
|
672
687
|
alert("Failed to create topic")
|
|
673
688
|
}
|
|
@@ -796,8 +811,12 @@ export default class extends Controller {
|
|
|
796
811
|
if (!data.topic) return
|
|
797
812
|
|
|
798
813
|
const topics = this.topics || []
|
|
799
|
-
const
|
|
800
|
-
if (
|
|
814
|
+
const existsById = topics.some((topic) => String(topic.id) === String(data.topic.id))
|
|
815
|
+
if (existsById) return
|
|
816
|
+
// Prevent duplicate topic names (e.g. two "Main" topics from race between
|
|
817
|
+
// HTTP loadTopics and WebSocket broadcast)
|
|
818
|
+
const existsByName = data.topic.name && topics.some((topic) => topic.name === data.topic.name)
|
|
819
|
+
if (existsByName) return
|
|
801
820
|
|
|
802
821
|
this.topics = [...topics, data.topic]
|
|
803
822
|
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
@@ -837,7 +856,7 @@ export default class extends Controller {
|
|
|
837
856
|
const index = topics.findIndex(t => String(t.id) === String(updatedTopic.id))
|
|
838
857
|
if (index === -1) return
|
|
839
858
|
|
|
840
|
-
this.topics[index] = updatedTopic
|
|
859
|
+
this.topics[index] = { ...this.topics[index], ...updatedTopic }
|
|
841
860
|
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
842
861
|
this.restoreSelection()
|
|
843
862
|
}
|
|
@@ -852,7 +871,7 @@ export default class extends Controller {
|
|
|
852
871
|
this.topics = nextTopics
|
|
853
872
|
if (String(this.currentTopicId) === String(topicId)) {
|
|
854
873
|
this.currentTopicId = ""
|
|
855
|
-
this.dispatch("change", { detail: { topicId: "" } })
|
|
874
|
+
this.dispatch("change", { detail: { topicId: "", mainTopicId: this.mainTopicId } })
|
|
856
875
|
}
|
|
857
876
|
|
|
858
877
|
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
@@ -901,7 +920,7 @@ export default class extends Controller {
|
|
|
901
920
|
this.currentTopicId = result.topic.id
|
|
902
921
|
await this.flushSaveLastTopic(result.topic.id)
|
|
903
922
|
await this.loadTopics()
|
|
904
|
-
this.dispatch("change", { detail: { topicId: result.topic.id } })
|
|
923
|
+
this.dispatch("change", { detail: { topicId: result.topic.id, mainTopicId: this.mainTopicId } })
|
|
905
924
|
|
|
906
925
|
// Clear selection in list controller
|
|
907
926
|
const listController = this.application.getControllerForElementAndIdentifier(
|
|
@@ -956,7 +975,7 @@ export default class extends Controller {
|
|
|
956
975
|
this.currentTopicId = topic.id
|
|
957
976
|
await this.flushSaveLastTopic(topic.id)
|
|
958
977
|
await this.loadTopics()
|
|
959
|
-
this.dispatch("change", { detail: { topicId: topic.id } })
|
|
978
|
+
this.dispatch("change", { detail: { topicId: topic.id, mainTopicId: this.mainTopicId } })
|
|
960
979
|
} else {
|
|
961
980
|
console.error('Failed to create topic with agent')
|
|
962
981
|
}
|
|
@@ -85,6 +85,7 @@ function applyRowProperties(row, node) {
|
|
|
85
85
|
updateBooleanAttr('expanded', 'expanded', node.expanded)
|
|
86
86
|
updateBooleanAttr('isRoot', 'is-root', node.is_root)
|
|
87
87
|
updateBooleanAttr('archived', 'archived', node.archived)
|
|
88
|
+
updateBooleanAttr('githubSource', 'github-source', node.github_source)
|
|
88
89
|
|
|
89
90
|
if (node.link_url) {
|
|
90
91
|
if (row.linkUrl !== node.link_url) {
|
|
@@ -36,6 +36,37 @@ describe('ChatNavigationHistory', () => {
|
|
|
36
36
|
expect(history.current().snippet).toBe('A updated')
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
+
test('revisit reorders entry to reflect visit order', () => {
|
|
40
|
+
history.push({ creativeId: '1', snippet: 'A' })
|
|
41
|
+
history.push({ creativeId: '2', snippet: 'B' })
|
|
42
|
+
history.push({ creativeId: '3', snippet: 'C' })
|
|
43
|
+
history.push({ creativeId: '4', snippet: 'D' })
|
|
44
|
+
// entries: [A, B, C, D], cursor at D (index 3)
|
|
45
|
+
|
|
46
|
+
history.push({ creativeId: '2', snippet: 'B' })
|
|
47
|
+
// B should move after D: [A, C, D, B], cursor at B (index 3)
|
|
48
|
+
expect(history.entries.map(e => e.creativeId)).toEqual(['1', '3', '4', '2'])
|
|
49
|
+
expect(history.current().creativeId).toBe('2')
|
|
50
|
+
|
|
51
|
+
// prev should go back to D (where we came from)
|
|
52
|
+
expect(history.prev().creativeId).toBe('4')
|
|
53
|
+
expect(history.prev().creativeId).toBe('3')
|
|
54
|
+
expect(history.prev().creativeId).toBe('1')
|
|
55
|
+
// wraps around
|
|
56
|
+
expect(history.prev().creativeId).toBe('2')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('revisit current entry does not reorder', () => {
|
|
60
|
+
history.push({ creativeId: '1', snippet: 'A' })
|
|
61
|
+
history.push({ creativeId: '2', snippet: 'B' })
|
|
62
|
+
history.push({ creativeId: '3', snippet: 'C' })
|
|
63
|
+
|
|
64
|
+
history.push({ creativeId: '3', snippet: 'C updated' })
|
|
65
|
+
expect(history.entries.map(e => e.creativeId)).toEqual(['1', '2', '3'])
|
|
66
|
+
expect(history.current().creativeId).toBe('3')
|
|
67
|
+
expect(history.current().snippet).toBe('C updated')
|
|
68
|
+
})
|
|
69
|
+
|
|
39
70
|
test('prev cycles backward', () => {
|
|
40
71
|
history.push({ creativeId: '1', snippet: 'A' })
|
|
41
72
|
history.push({ creativeId: '2', snippet: 'B' })
|
|
@@ -33,10 +33,20 @@ class ChatNavigationHistory {
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
if (existingIdx !== -1) {
|
|
36
|
-
// Already in list — update metadata and move cursor there
|
|
37
36
|
this.entries[existingIdx].snippet = entry.snippet || this.entries[existingIdx].snippet
|
|
38
37
|
this.entries[existingIdx].canComment = !!entry.canComment
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
if (existingIdx === this.currentIndex) {
|
|
40
|
+
this._save()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Move to after current position to reflect visit order (LRU)
|
|
45
|
+
const [moved] = this.entries.splice(existingIdx, 1)
|
|
46
|
+
if (existingIdx < this.currentIndex) this.currentIndex--
|
|
47
|
+
const insertAt = this.currentIndex + 1
|
|
48
|
+
this.entries.splice(insertAt, 0, moved)
|
|
49
|
+
this.currentIndex = insertAt
|
|
40
50
|
this._save()
|
|
41
51
|
return
|
|
42
52
|
}
|
|
@@ -68,11 +68,15 @@ function sanitizeLang(lang) {
|
|
|
68
68
|
return lang.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Custom renderer for code blocks with syntax highlighting
|
|
71
|
+
// Custom renderer for code blocks with syntax highlighting + mermaid
|
|
72
72
|
marked.use({
|
|
73
73
|
renderer: {
|
|
74
74
|
code({ text, lang }) {
|
|
75
75
|
const safeLang = sanitizeLang(lang)
|
|
76
|
+
if (safeLang === 'mermaid') {
|
|
77
|
+
const escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
78
|
+
return `<div class="mermaid-chart">${escaped}</div>`
|
|
79
|
+
}
|
|
76
80
|
const highlighted = highlightCode(text, safeLang)
|
|
77
81
|
const langClass = safeLang ? ` language-${safeLang}` : ''
|
|
78
82
|
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
@@ -99,6 +103,14 @@ DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
|
99
103
|
data.forceKeepAttr = true
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
// Allow mermaid-chart class on div elements
|
|
107
|
+
if (node.tagName === 'DIV' && data.attrName === 'class') {
|
|
108
|
+
const classes = data.attrValue.split(/\s+/)
|
|
109
|
+
if (classes.includes('mermaid-chart')) {
|
|
110
|
+
data.attrValue = 'mermaid-chart'
|
|
111
|
+
data.forceKeepAttr = true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
102
114
|
})
|
|
103
115
|
|
|
104
116
|
const PURIFY_CONFIG = {
|
|
@@ -125,6 +137,24 @@ export function renderCommentMarkdown(text) {
|
|
|
125
137
|
return sanitize(html.trim())
|
|
126
138
|
}
|
|
127
139
|
|
|
140
|
+
// Lazy-load mermaid and render diagrams in a container
|
|
141
|
+
let mermaidReady = false
|
|
142
|
+
|
|
143
|
+
export async function renderMermaidDiagrams(container) {
|
|
144
|
+
const charts = container.querySelectorAll('.mermaid-chart:not([data-processed])')
|
|
145
|
+
if (charts.length === 0) return
|
|
146
|
+
const { default: mermaid } = await import('mermaid')
|
|
147
|
+
if (!mermaidReady) {
|
|
148
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' })
|
|
149
|
+
mermaidReady = true
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await mermaid.run({ nodes: Array.from(charts) })
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn('Mermaid rendering failed:', e)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
export function renderMarkdownInContainer(container) {
|
|
129
159
|
container.querySelectorAll('.comment-content').forEach((element) => {
|
|
130
160
|
if (element.dataset.rendered === 'true') return
|
|
@@ -132,4 +162,5 @@ export function renderMarkdownInContainer(container) {
|
|
|
132
162
|
element.dataset.rendered = 'true'
|
|
133
163
|
addTableDownloadButtons(element)
|
|
134
164
|
})
|
|
165
|
+
renderMermaidDiagrams(container)
|
|
135
166
|
}
|
|
@@ -241,6 +241,14 @@ export default class CommandArgsForm {
|
|
|
241
241
|
input.dataset.paramRequired = param.required ? 'true' : 'false'
|
|
242
242
|
if (param.description) input.placeholder = param.description
|
|
243
243
|
|
|
244
|
+
// Pre-fill with default value from creative tool defaults
|
|
245
|
+
if (param.default_value != null) {
|
|
246
|
+
input.value = String(param.default_value)
|
|
247
|
+
if (input.tagName === 'TEXTAREA') {
|
|
248
|
+
requestAnimationFrame(() => this._autoResize(input))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
244
252
|
// Enter submits; Shift+Enter inserts newline (textarea only)
|
|
245
253
|
input.addEventListener('keydown', (e) => {
|
|
246
254
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -1095,11 +1095,9 @@ export function initializeCreativeRowEditor() {
|
|
|
1095
1095
|
// Notify sync controller that editing stopped
|
|
1096
1096
|
stopEditingPing();
|
|
1097
1097
|
const editCreativeId = form.dataset.creativeId;
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}));
|
|
1102
|
-
}
|
|
1098
|
+
document.dispatchEvent(new CustomEvent('creative-editing:stop', {
|
|
1099
|
+
detail: { creativeId: editCreativeId ? parseInt(editCreativeId, 10) : null }
|
|
1100
|
+
}));
|
|
1103
1101
|
|
|
1104
1102
|
currentTree = null;
|
|
1105
1103
|
currentRowElement = null;
|
|
@@ -1749,6 +1747,9 @@ export function initializeCreativeRowEditor() {
|
|
|
1749
1747
|
pendingSave = false;
|
|
1750
1748
|
lexicalEditor.focus();
|
|
1751
1749
|
updateActionButtonStates();
|
|
1750
|
+
document.dispatchEvent(new CustomEvent('creative-editing:start', {
|
|
1751
|
+
detail: { creativeId: null }
|
|
1752
|
+
}));
|
|
1752
1753
|
if (parentSuggestions) {
|
|
1753
1754
|
parentSuggestions.style.display = 'none';
|
|
1754
1755
|
parentSuggestions.innerHTML = '';
|
|
@@ -1786,17 +1787,13 @@ export function initializeCreativeRowEditor() {
|
|
|
1786
1787
|
scheduleSave();
|
|
1787
1788
|
}
|
|
1788
1789
|
|
|
1789
|
-
// Intercepts
|
|
1790
|
-
// Returning true triggers preventDefault + stopImmediatePropagation
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
// Mobile: Enter → newline (let Lexical handle it)
|
|
1790
|
+
// Intercepts Shift+Enter via capture-phase keydown on Lexical's root element.
|
|
1791
|
+
// Returning true triggers preventDefault + stopImmediatePropagation.
|
|
1792
|
+
// Shift+Enter → addNew (save & add next)
|
|
1793
|
+
// Bare Enter → newline (handled by Lexical)
|
|
1794
1794
|
function handleEditorEnterKey(event, editorInstance) {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
return true; // handled — suppress Lexical's newline
|
|
1798
|
-
}
|
|
1799
|
-
return false; // mobile: let Lexical insert newline
|
|
1795
|
+
addNew();
|
|
1796
|
+
return true; // handled — suppress default behavior
|
|
1800
1797
|
}
|
|
1801
1798
|
|
|
1802
1799
|
function handleEditorKeyDown(event, editorInstance) {
|
|
@@ -1811,8 +1808,6 @@ export function initializeCreativeRowEditor() {
|
|
|
1811
1808
|
addChild();
|
|
1812
1809
|
return;
|
|
1813
1810
|
}
|
|
1814
|
-
// Note: bare Enter is handled by handleEditorEnterKey via capture-phase keydown
|
|
1815
|
-
// on Lexical's root element to prevent newline insertion before addNew().
|
|
1816
1811
|
if ((event.ctrlKey || event.metaKey) && event.shiftKey && (event.key === '.' || event.key === '>')) {
|
|
1817
1812
|
event.preventDefault();
|
|
1818
1813
|
levelDown();
|