Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
43fc2bc
fix: rename SmartPicker to "Add from Nextcloud" and change click beha…
chrip Jun 18, 2026
0141832
feat: gate "Add from Nextcloud" on connection, trigger via "/", inser…
chrip Jun 19, 2026
8a8c5f0
feat(smartpicker): native menu at the caret, delegating to Nextcloud'…
chrip Aug 12, 2026
ba4ba34
feat(assistant): open Nextcloud's Smart Picker from the Insert button
chrip Aug 12, 2026
664b3af
refactor(assistant): drop the editor half of the unused op channel
chrip Aug 12, 2026
df0c4ff
fix(smartpicker): never append a link to a cell that already holds so…
chrip Aug 12, 2026
9d988fd
fix(smartpicker): type the trigger into the document, replace it on i…
chrip Aug 13, 2026
4760b6b
chore(license): correct the copyright header on the files this branch…
chrip Aug 14, 2026
8df5d28
test(smart-picker): register the SmartPicker suite in the test runner
chrip Aug 14, 2026
63ab934
fix(smartpicker): let the pending record outlast a person using the p…
chrip Aug 14, 2026
27be4c7
fix(smartpicker): do not delete the trigger unless it is still there
chrip Aug 14, 2026
f251927
fix(smartpicker): repair the "/" caret session, from PR review
chrip Aug 18, 2026
5fed4b7
fix(smartpicker): follow the caret when the window is resized
chrip Aug 18, 2026
1dd0415
fix(assistant): stop losing a result the editor was too busy to take
chrip Aug 18, 2026
75e66e0
fix(smartpicker): aim the reply at the request that asked for it
chrip Aug 18, 2026
0b9fd09
fix(icons): give "Ask Nextcloud Assistant" its own icon back
chrip Aug 18, 2026
b5ed4bd
chore(build): drop the mobile index.html churn from this branch
chrip Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/api/documents/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,38 @@
});
};

var _setSmartPickerAvailable = function(data) {
_sendCommand({
command: 'setSmartPickerAvailable',
data: data
});
};

// data: {providers: [{id, title, icon_url}]}. An object, not a bare array:
// Gateway relays these through jQuery's trigger(), which spreads an array
// into separate handler arguments, so a bare array arrives as its first
// element. Every sibling command here passes an object for the same reason.
var _setSmartPickerProviders = function(data) {
_sendCommand({
command: 'setSmartPickerProviders',
data: data
});
};

// data: {html, text}. Insert an Assistant result, keeping its formatting.
var _insertAssistantResult = function(data) {
_sendCommand({
command: 'insertAssistantResult',
data: data
});
};

var _setSmartPickerCancel = function() {
_sendCommand({
command: 'setSmartPickerCancel'
});
};

var _setMailMergeRecipients = function(data) {
_sendCommand({
command: 'setMailMergeRecipients',
Expand Down Expand Up @@ -934,6 +966,10 @@
insertLink : _insertLink,
insertPlainText : _insertPlainText,
setAssistantAvailable : _setAssistantAvailable,
setSmartPickerAvailable : _setSmartPickerAvailable,
setSmartPickerProviders : _setSmartPickerProviders,
insertAssistantResult : _insertAssistantResult,
setSmartPickerCancel : _setSmartPickerCancel,
setMailMergeRecipients: _setMailMergeRecipients,
setRevisedFile : _setRevisedFile,
setFavorite : _setFavorite,
Expand Down
27 changes: 25 additions & 2 deletions apps/common/Gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ if (window.Common === undefined) {
$me.trigger('setassistantavailable', data);
},

'setSmartPickerAvailable': function(data) {
$me.trigger('setsmartpickeravailable', data);
},

'setSmartPickerProviders': function(data) {
$me.trigger('setsmartpickerproviders', data);
},

'insertAssistantResult': function(data) {
$me.trigger('insertassistantresult', data);
},

'setSmartPickerCancel': function() {
$me.trigger('setsmartpickercancel');
},

'setMailMergeRecipients': function(data) {
$me.trigger('setmailmergerecipients', data);
},
Expand Down Expand Up @@ -452,8 +468,15 @@ if (window.Common === undefined) {
_postMessage({event: 'onSubmit'});
},

requestSmartPicker: function(selectedText, source) {
_postMessage({event: 'onRequestSmartPicker', data: { selectedText: selectedText || '', source: source || 'smartpicker' }});
// providerId targets one Nextcloud picker provider directly, so the
// editor can present the provider list itself instead of showing
// the Nextcloud provider-selection modal.
requestSmartPicker: function(selectedText, source, providerId) {
_postMessage({event: 'onRequestSmartPicker', data: {
selectedText: selectedText || '',
source: source || 'smartpicker',
providerId: providerId || ''
}});
},

on: function(event, handler){
Expand Down
78 changes: 78 additions & 0 deletions apps/common/main/lib/util/AssistantInsert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH or an Nextcloud affiliate company and Euro-Office contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/*
* Insert an Assistant result into the document, keeping its formatting.
*
* The result arrives from the host as HTML, converted there from the markdown
* these models emit. Plain-text paste would drop headings, lists and emphasis,
* which is why this goes through pluginMethod_PasteHtml.
*/
define([], function () { 'use strict';

Common.Utils = Common.Utils || {};

// sdkjs parks this element on the page for the duration of a PasteHtml and
// refuses a second one while it is there (api_plugins.js).
var PASTE_GUARD_ID = 'pmpastehtml';

// 100 ms x 20 = 2 s. A paste of a few paragraphs is done in a fraction of
// that; anything still running after it is not about to finish either.
var RETRY_INTERVAL = 100,
MAX_ATTEMPTS = 20;

Common.Utils.AssistantInsert = _.extend({

/**
* @param {Object} api the editor api
* @param {Object} result {html, text}
* @param {Number} attempt internal, counts retries
*/
insert: function(api, result, attempt) {
if (!api || !result) return;
attempt = attempt || 0;

var html = result.html,
text = result.text || '';

if (html && typeof api['pluginMethod_PasteHtml'] === 'function') {
// PasteHtml is re-entrancy guarded on this element, so a second
// insertion while one is still running is dropped. Wait it out
// rather than losing the result.
if (document.getElementById(PASTE_GUARD_ID)) {
if (attempt < MAX_ATTEMPTS) {
setTimeout(function() {
Common.Utils.AssistantInsert.insert(api, result, attempt + 1);
}, RETRY_INTERVAL);
return;
}
// Still guarded. Calling PasteHtml anyway is not a fallback:
// the guard drops it without a word, and the answer the user
// waited for would simply never appear in the document.
// Plain text loses the formatting but keeps the content, so
// try that; only when there is none is there nothing left to
// do but say so.
if (text && typeof api['pluginMethod_PasteText'] === 'function') {
api['pluginMethod_PasteText'](text);
Common.NotificationCenter.trigger('edit:complete');
return;
}
Common.UI.warning({msg: Common.Utils.AssistantInsert.txtInsertFailed});
return;
}
api['pluginMethod_PasteHtml'](html);
} else if (typeof api['pluginMethod_PasteText'] === 'function') {
api['pluginMethod_PasteText'](text);
}

Common.NotificationCenter.trigger('edit:complete');
},

txtInsertFailed: 'The editor is busy, so the Assistant result could not be inserted. Try inserting it again.'

}, Common.Utils.AssistantInsert || {});

return Common.Utils.AssistantInsert;
});
Loading
Loading