feat(ai): services partages A2 + SSE streaming B4 + confirmations UI B5 (#79)
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
This commit is contained in:
@@ -4203,6 +4203,11 @@
|
||||
(recharger ou supprimer) ; « + » démarre une nouvelle
|
||||
conversation.
|
||||
</li>
|
||||
<li data-i18n="help.assistant_agent">
|
||||
Le bouton « mode agent » active les outils (lire, lister,
|
||||
chercher) ; les actions de modification demandent une
|
||||
confirmation avec aperçu des changements.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
+307
-74
@@ -74,6 +74,36 @@ class BooksLM {
|
||||
this._sessions = [];
|
||||
this._currentSessionId = null;
|
||||
this._pendingNewSession = false;
|
||||
// Agent mode: routes chat through /api/ai/bookslm/agent so the model can
|
||||
// call read/search tools and propose mutations (confirmation cards).
|
||||
this._agentMode = this._readAgentMode();
|
||||
}
|
||||
|
||||
_readAgentMode() {
|
||||
try {
|
||||
return localStorage.getItem('obsigate-bookslm-agent') === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_toggleAgentMode() {
|
||||
this._agentMode = !this._agentMode;
|
||||
try {
|
||||
localStorage.setItem('obsigate-bookslm-agent', this._agentMode ? 'true' : 'false');
|
||||
} catch { /* private mode */ }
|
||||
this._updateAgentToggle();
|
||||
}
|
||||
|
||||
_updateAgentToggle() {
|
||||
if (!this._panel) return;
|
||||
const btn = this._panel.querySelector('.bookslm-btn-agent');
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('active', this._agentMode);
|
||||
const label = this._agentMode ? t('ai.agent_mode_on') : t('ai.agent_mode_off');
|
||||
btn.title = label;
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.setAttribute('aria-pressed', this._agentMode ? 'true' : 'false');
|
||||
}
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
@@ -376,6 +406,7 @@ class BooksLM {
|
||||
<span class="bookslm-subtitle"></span>
|
||||
</div>
|
||||
<div class="bookslm-header-actions">
|
||||
<button class="bookslm-btn-agent" title="${t('ai.agent_mode_off')}" aria-label="${t('ai.agent_mode_off')}" aria-pressed="false"><i data-lucide="bot" style="width:16px;height:16px"></i></button>
|
||||
<button class="bookslm-btn-history" title="${t('bookslm.session_history')}" aria-label="${t('bookslm.session_history')}"><i data-lucide="history" style="width:16px;height:16px"></i></button>
|
||||
<button class="bookslm-btn-new" title="${t('bookslm.new_conversation')}" aria-label="${t('bookslm.new_conversation')}"><i data-lucide="plus" style="width:16px;height:16px"></i></button>
|
||||
<button class="bookslm-btn-export" title="${t('bookslm.export')}" aria-label="${t('bookslm.export')}"><i data-lucide="download" style="width:16px;height:16px"></i></button>
|
||||
@@ -409,6 +440,7 @@ class BooksLM {
|
||||
|
||||
panel.querySelector('.bookslm-btn-close').addEventListener('click', () => this.close());
|
||||
panel.querySelector('.bookslm-btn-new').addEventListener('click', () => this.newConversation());
|
||||
panel.querySelector('.bookslm-btn-agent').addEventListener('click', () => this._toggleAgentMode());
|
||||
panel.querySelector('.bookslm-btn-history').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this._toggleHistoryMenu();
|
||||
@@ -567,12 +599,18 @@ class BooksLM {
|
||||
if (msg.role === 'assistant') {
|
||||
const { text, actions } = this._extractActions(msg.content || '');
|
||||
bubble.innerHTML = this._renderMarkdown(text);
|
||||
if (msg.toolCalls && msg.toolCalls.length) {
|
||||
bubble.insertBefore(this._renderToolActivity(msg.toolCalls), bubble.firstChild);
|
||||
}
|
||||
if (actions.length) {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'bookslm-actions';
|
||||
actions.forEach((action) => actionsDiv.appendChild(this._renderActionCard(action)));
|
||||
bubble.appendChild(actionsDiv);
|
||||
}
|
||||
if (msg.confirmation) {
|
||||
bubble.appendChild(this._renderConfirmationCard(msg));
|
||||
}
|
||||
if (msg.sources && msg.sources.length) {
|
||||
const sourcesDiv = document.createElement('div');
|
||||
sourcesDiv.className = 'bookslm-sources';
|
||||
@@ -673,6 +711,172 @@ class BooksLM {
|
||||
} catch { /* tree refresh is best-effort */ }
|
||||
}
|
||||
|
||||
// ── Tool activity & confirmations (agent mode) ──────────────────────
|
||||
|
||||
_renderToolActivity(toolCalls) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'bookslm-tool-trace';
|
||||
for (const call of toolCalls) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'bookslm-tool-line' + (call.ok === false ? ' failed' : '');
|
||||
line.textContent = `${call.ok === false ? '⚠' : '🔧'} ${t('ai.tool_call', { name: call.name })}`;
|
||||
if (call.ok === false) line.title = t('ai.tool_call_failed', { name: call.name });
|
||||
wrap.appendChild(line);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
_renderConfirmationCard(msg) {
|
||||
const conf = msg.confirmation;
|
||||
const pending = conf.pending || {};
|
||||
const error = pending.error || pending;
|
||||
const tool = error.tool || 'action';
|
||||
const args = error.arguments || {};
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bookslm-action bookslm-confirm';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'bookslm-action-meta';
|
||||
meta.innerHTML = '<span class="bookslm-action-icon">🔒</span>';
|
||||
const textEl = document.createElement('span');
|
||||
textEl.textContent = t('ai.tool_call', { name: tool }) + (args.path ? ` — ${args.path}` : '');
|
||||
meta.appendChild(textEl);
|
||||
card.appendChild(meta);
|
||||
|
||||
const diffHost = document.createElement('div');
|
||||
diffHost.className = 'bookslm-confirm-diff';
|
||||
if (conf._diffHtml) {
|
||||
diffHost.innerHTML = conf._diffHtml;
|
||||
} else if (!conf._diffLoading) {
|
||||
conf._diffLoading = true;
|
||||
this._fillConfirmationDiff(args, conf).then(() => this._renderMessages());
|
||||
}
|
||||
card.appendChild(diffHost);
|
||||
|
||||
const apply = document.createElement('button');
|
||||
apply.className = 'bookslm-action-apply';
|
||||
apply.textContent = t('ai.action_apply');
|
||||
apply.addEventListener('click', async () => {
|
||||
apply.disabled = true;
|
||||
apply.textContent = t('ai.action_applying');
|
||||
try {
|
||||
await this._applyConfirmation(msg);
|
||||
apply.textContent = t('ai.action_applied');
|
||||
} catch (e) {
|
||||
apply.disabled = false;
|
||||
apply.textContent = t('ai.action_apply');
|
||||
showToast(t('ai.action_failed', { error: e.message }), 'error');
|
||||
}
|
||||
});
|
||||
card.appendChild(apply);
|
||||
return card;
|
||||
}
|
||||
|
||||
async _fillConfirmationDiff(args, conf) {
|
||||
const proposed = args.content;
|
||||
if (typeof proposed !== 'string' || !args.vault || !args.path) return;
|
||||
let current = null;
|
||||
try {
|
||||
current = await this._fetchCurrentContent(args.vault, args.path);
|
||||
} catch {
|
||||
current = null;
|
||||
}
|
||||
const title = `<div class="bookslm-diff-title">${this._escapeHtml(t('ai.diff_preview'))}</div>`;
|
||||
if (current == null) {
|
||||
// New file (or unreadable): show everything as an addition.
|
||||
conf._diffHtml = title + `<pre class="bookslm-diff">${this._renderDiffLines([{ t: '+', s: proposed }])}</pre>`;
|
||||
return;
|
||||
}
|
||||
const lines = this._lineDiff(current, proposed);
|
||||
conf._diffHtml = title + `<pre class="bookslm-diff">${this._renderDiffLines(lines)}</pre>`;
|
||||
}
|
||||
|
||||
async _fetchCurrentContent(vault, path) {
|
||||
const data = await api(`/api/file/${encodeURIComponent(vault)}/raw?path=${encodeURIComponent(path)}`);
|
||||
return data && typeof data.raw === 'string' ? data.raw : '';
|
||||
}
|
||||
|
||||
/** Minimal line diff (LCS) between the current and the proposed content. */
|
||||
_lineDiff(oldText, newText) {
|
||||
const a = String(oldText || '').split('\n');
|
||||
const b = String(newText || '').split('\n');
|
||||
const n = a.length;
|
||||
const m = b.length;
|
||||
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
for (let j = m - 1; j >= 0; j--) {
|
||||
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
||||
}
|
||||
}
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < n && j < m) {
|
||||
if (a[i] === b[j]) { out.push({ t: ' ', s: a[i] }); i++; j++; }
|
||||
else if (dp[i + 1][j] >= dp[i][j + 1]) { out.push({ t: '-', s: a[i] }); i++; }
|
||||
else { out.push({ t: '+', s: b[j] }); j++; }
|
||||
}
|
||||
while (i < n) out.push({ t: '-', s: a[i++] });
|
||||
while (j < m) out.push({ t: '+', s: b[j++] });
|
||||
return out;
|
||||
}
|
||||
|
||||
_renderDiffLines(lines) {
|
||||
const cap = 400;
|
||||
const shown = lines.length > cap ? lines.slice(0, cap) : lines;
|
||||
let html = shown.map((l) => {
|
||||
const cls = l.t === '+' ? 'bookslm-diff-add' : (l.t === '-' ? 'bookslm-diff-del' : 'bookslm-diff-ctx');
|
||||
return `<span class="${cls}">${this._escapeHtml(l.t + ' ' + l.s)}</span>`;
|
||||
}).join('\n');
|
||||
if (lines.length > cap) {
|
||||
html += `\n<span class="bookslm-diff-ctx">… ${lines.length - cap}</span>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
async _applyConfirmation(msg) {
|
||||
const conf = msg.confirmation;
|
||||
if (!conf || conf._applying) return;
|
||||
conf._applying = true;
|
||||
|
||||
const payload = {
|
||||
...(msg.payload || {}),
|
||||
confirm: conf.pending,
|
||||
confirm_messages: conf.messages,
|
||||
};
|
||||
|
||||
this._isLoading = true;
|
||||
this._abortCtrl = new AbortController();
|
||||
const sendBtn = this._panel && this._panel.querySelector('.bookslm-btn-send');
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
const continuation = { role: 'assistant', content: '', sources: [], toolCalls: [], confirmation: null, payload: null };
|
||||
try {
|
||||
let resp = await this._postChat(payload);
|
||||
if (resp.status === 401 && AuthManager._authEnabled) {
|
||||
await AuthManager.refreshAccessToken();
|
||||
resp = await this._postChat(payload);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail = `HTTP ${resp.status}`;
|
||||
try { detail = (await resp.json()).detail || detail; } catch { /* */ }
|
||||
throw new Error(detail);
|
||||
}
|
||||
// The confirmation is resolved: drop the card and show the continuation.
|
||||
msg.confirmation = null;
|
||||
this._messages.push(continuation);
|
||||
await this._streamResponse(resp, continuation, payload);
|
||||
} finally {
|
||||
conf._applying = false;
|
||||
this._isLoading = false;
|
||||
this._abortCtrl = null;
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
this._renderMessages();
|
||||
this._saveHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Messaging ───────────────────────────────────────────────────────
|
||||
|
||||
async _sendMessage() {
|
||||
@@ -694,7 +898,26 @@ class BooksLM {
|
||||
this._messages.push({ role: 'user', content: text });
|
||||
this._renderMessages();
|
||||
|
||||
const assistantMsg = { role: 'assistant', content: '', sources: [] };
|
||||
let provider = null;
|
||||
let model = null;
|
||||
try {
|
||||
const picker = JSON.parse(localStorage.getItem('obsigate_ai_picker') || '{}');
|
||||
provider = picker.provider || null;
|
||||
model = picker.model || null;
|
||||
} catch { /* */ }
|
||||
|
||||
const payload = {
|
||||
mode: this._mode,
|
||||
vault: this._vault,
|
||||
directory: this._directory,
|
||||
context_files: this._mode === MODE.DOCUMENTS ? this._documents.map((d) => d.path) : [],
|
||||
message: text,
|
||||
conversation_history: history,
|
||||
provider,
|
||||
model,
|
||||
};
|
||||
|
||||
const assistantMsg = { role: 'assistant', content: '', sources: [], toolCalls: [], confirmation: null, payload };
|
||||
this._messages.push(assistantMsg);
|
||||
this._renderMessages();
|
||||
this._isLoading = true;
|
||||
@@ -705,39 +928,10 @@ class BooksLM {
|
||||
this._abortCtrl = new AbortController();
|
||||
|
||||
try {
|
||||
let provider = null;
|
||||
let model = null;
|
||||
try {
|
||||
const picker = JSON.parse(localStorage.getItem('obsigate_ai_picker') || '{}');
|
||||
provider = picker.provider || null;
|
||||
model = picker.model || null;
|
||||
} catch { /* */ }
|
||||
|
||||
const payload = {
|
||||
mode: this._mode,
|
||||
vault: this._vault,
|
||||
directory: this._directory,
|
||||
context_files: this._mode === MODE.DOCUMENTS ? this._documents.map((d) => d.path) : [],
|
||||
message: text,
|
||||
conversation_history: history,
|
||||
provider,
|
||||
model,
|
||||
};
|
||||
|
||||
const doFetch = () => {
|
||||
const headers = { 'Content-Type': 'application/json', ...(AuthManager.getAuthHeaders() || {}) };
|
||||
return fetch('/api/ai/bookslm/chat', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
signal: this._abortCtrl.signal,
|
||||
});
|
||||
};
|
||||
let resp = await doFetch();
|
||||
let resp = await this._postChat(payload);
|
||||
if (resp.status === 401 && AuthManager._authEnabled) {
|
||||
await AuthManager.refreshAccessToken();
|
||||
resp = await doFetch();
|
||||
resp = await this._postChat(payload);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail = `HTTP ${resp.status}`;
|
||||
@@ -745,46 +939,7 @@ class BooksLM {
|
||||
throw new Error(detail);
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let currentEvent = 'message';
|
||||
|
||||
const handleLine = (line) => {
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.slice(6).trim();
|
||||
return;
|
||||
}
|
||||
if (!line.startsWith('data:')) return;
|
||||
const raw = line.slice(5).trim();
|
||||
if (!raw || raw === '[DONE]') return;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
assistantMsg.content += raw;
|
||||
return;
|
||||
}
|
||||
if (currentEvent === 'error' || data.error) {
|
||||
const msg = data.error || t('bookslm.error');
|
||||
assistantMsg.content += (assistantMsg.content ? '\n\n' : '') + '⚠ ' + msg;
|
||||
return;
|
||||
}
|
||||
const chunk = data.token || data.content || '';
|
||||
if (chunk) assistantMsg.content += chunk;
|
||||
if (data.sources) assistantMsg.sources = data.sources;
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) handleLine(line);
|
||||
this._renderMessages();
|
||||
}
|
||||
if (buffer) handleLine(buffer);
|
||||
await this._streamResponse(resp, assistantMsg, payload);
|
||||
|
||||
if (!assistantMsg.sources.length && this._contextFiles.length) {
|
||||
const contentLower = assistantMsg.content.toLowerCase();
|
||||
@@ -809,6 +964,78 @@ class BooksLM {
|
||||
this._saveHistory();
|
||||
}
|
||||
|
||||
/** POST to /chat or /agent depending on the agent-mode toggle. */
|
||||
_postChat(payload) {
|
||||
const endpoint = this._agentMode ? '/api/ai/bookslm/agent' : '/api/ai/bookslm/chat';
|
||||
const headers = { 'Content-Type': 'application/json', ...(AuthManager.getAuthHeaders() || {}) };
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
signal: this._abortCtrl ? this._abortCtrl.signal : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume an SSE response, feeding tokens/tool/confirmation events into
|
||||
* `assistantMsg`. Shared by the initial send and the confirmation resume.
|
||||
*/
|
||||
async _streamResponse(resp, assistantMsg, payload) {
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let currentEvent = 'message';
|
||||
|
||||
const handleLine = (line) => {
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.slice(6).trim();
|
||||
return;
|
||||
}
|
||||
if (!line.startsWith('data:')) return;
|
||||
const raw = line.slice(5).trim();
|
||||
if (!raw || raw === '[DONE]') return;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
assistantMsg.content += raw;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentEvent === 'tool') {
|
||||
assistantMsg.toolCalls.push({ name: data.name, ok: data.ok !== false, arguments: data.arguments || {} });
|
||||
return;
|
||||
}
|
||||
if (currentEvent === 'confirmation') {
|
||||
assistantMsg.confirmation = {
|
||||
pending: data.pending || data,
|
||||
messages: data.messages || [],
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (currentEvent === 'error' || data.error) {
|
||||
const msg = data.error || t('bookslm.error');
|
||||
assistantMsg.content += (assistantMsg.content ? '\n\n' : '') + '⚠ ' + msg;
|
||||
return;
|
||||
}
|
||||
const chunk = data.token || data.content || '';
|
||||
if (chunk) assistantMsg.content += chunk;
|
||||
if (data.sources) assistantMsg.sources = data.sources;
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) handleLine(line);
|
||||
this._renderMessages();
|
||||
}
|
||||
if (buffer) handleLine(buffer);
|
||||
}
|
||||
|
||||
// ── Markdown (self-contained renderer) ──────────────────────────────
|
||||
//
|
||||
// Renders the assistant answer as formatted HTML (headings, lists, tables,
|
||||
@@ -1167,14 +1394,20 @@ class BooksLM {
|
||||
} catch { /* quota / private mode */ }
|
||||
}
|
||||
|
||||
/** Strip transient (large) fields before persisting a conversation. */
|
||||
_persistableMessages() {
|
||||
return this._messages.map(({ confirmation, payload, ...rest }) => rest);
|
||||
}
|
||||
|
||||
_saveHistory() {
|
||||
const persisted = this._persistableMessages();
|
||||
const session = this._currentSession();
|
||||
if (session) {
|
||||
session.messages = this._messages;
|
||||
session.messages = persisted;
|
||||
session.updatedAt = Date.now();
|
||||
if (!session.title) session.title = this._deriveTitle(this._messages);
|
||||
} else {
|
||||
const created = this._createSession(this._messages);
|
||||
const created = this._createSession(persisted);
|
||||
created.title = this._deriveTitle(this._messages);
|
||||
this._sessions.unshift(created);
|
||||
this._currentSessionId = created.id;
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
"ai.action_created_dir": "Folder created: {path}",
|
||||
"ai.action_created_file": "File created: {path}",
|
||||
"ai.action_failed": "Action failed: {error}",
|
||||
"ai.agent_mode_off": "Agent mode off (read/search + actions)",
|
||||
"ai.agent_mode_on": "Agent mode on (read/search tools + actions)",
|
||||
"ai.casual": "Casual tone",
|
||||
"ai.close": "Close assistant",
|
||||
"ai.completion_added": "AI: completion added",
|
||||
@@ -94,6 +96,7 @@
|
||||
"ai.context_general_desc": "Questions about the app and file creation",
|
||||
"ai.continue": "Continue",
|
||||
"ai.custom_rewrite": "Custom rewrite",
|
||||
"ai.diff_preview": "Change preview",
|
||||
"ai.done": "AI: text processed",
|
||||
"ai.edit": "Edit",
|
||||
"ai.error_invalid_key": "Invalid API key",
|
||||
@@ -149,6 +152,8 @@
|
||||
"ai.to_list": "Convert to list",
|
||||
"ai.to_table": "Convert to table",
|
||||
"ai.tone_hint": "Changes the text tone",
|
||||
"ai.tool_call": "Tool: {name}",
|
||||
"ai.tool_call_failed": "Tool {name} failed",
|
||||
"ai.toolbar_edit": "Edit",
|
||||
"ai.toolbar_generate": "Generate",
|
||||
"ai.toolbar_rewrite": "Rewrite",
|
||||
@@ -1212,6 +1217,7 @@
|
||||
"help.assistant_markdown": "Formatted answers: headings, lists, tables, quotes and code blocks.",
|
||||
"help.assistant_links": "Cited files and paths are links: click a file to open it, a folder to reveal it in the tree.",
|
||||
"help.assistant_sessions": "The header history icon lists past sessions (reopen or delete); “+” starts a new conversation.",
|
||||
"help.assistant_agent": "The \"agent mode\" button enables tools (read, list, search); modifying actions require confirmation with a change preview.",
|
||||
"help.toolbar_section": "AI toolbar",
|
||||
"help.tree_section": "Vault tree",
|
||||
"help.use_cases": "Use cases",
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
"ai.action_created_dir": "Dossier créé : {path}",
|
||||
"ai.action_created_file": "Fichier créé : {path}",
|
||||
"ai.action_failed": "Échec de l'action : {error}",
|
||||
"ai.agent_mode_off": "Mode agent désactivé (lecture/recherche + actions)",
|
||||
"ai.agent_mode_on": "Mode agent activé (outils de lecture/recherche + actions)",
|
||||
"ai.casual": "Ton décontracté",
|
||||
"ai.close": "Fermer l'assistant",
|
||||
"ai.completion_added": "AI: complétion ajoutée",
|
||||
@@ -94,6 +96,7 @@
|
||||
"ai.context_general_desc": "Questions sur l'application et création de fichiers",
|
||||
"ai.continue": "Continuer",
|
||||
"ai.custom_rewrite": "Réécriture personnalisée",
|
||||
"ai.diff_preview": "Aperçu des modifications",
|
||||
"ai.done": "AI: texte traité",
|
||||
"ai.edit": "Éditer",
|
||||
"ai.error_invalid_key": "clé API invalide",
|
||||
@@ -149,6 +152,8 @@
|
||||
"ai.to_list": "Convertir en liste",
|
||||
"ai.to_table": "Convertir en tableau",
|
||||
"ai.tone_hint": "Change le ton du texte",
|
||||
"ai.tool_call": "Outil : {name}",
|
||||
"ai.tool_call_failed": "Outil {name} en échec",
|
||||
"ai.toolbar_edit": "Éditer",
|
||||
"ai.toolbar_generate": "Générer",
|
||||
"ai.toolbar_rewrite": "Réécrire",
|
||||
@@ -1212,6 +1217,7 @@
|
||||
"help.assistant_markdown": "Réponses formatées : titres, listes, tableaux, citations et blocs de code.",
|
||||
"help.assistant_links": "Les fichiers et chemins cités sont des liens : cliquez sur un fichier pour l'ouvrir, sur un dossier pour le révéler dans l'arborescence.",
|
||||
"help.assistant_sessions": "L'icône historique de l'en-tête liste les sessions passées (recharger ou supprimer) ; « + » démarre une nouvelle conversation.",
|
||||
"help.assistant_agent": "Le bouton « mode agent » active les outils (lire, lister, chercher) ; les actions de modification demandent une confirmation avec aperçu des changements.",
|
||||
"help.toolbar_section": "Barre d'outils AI",
|
||||
"help.tree_section": "Arborescence des vaults",
|
||||
"help.use_cases": "Cas d'usage",
|
||||
|
||||
@@ -9222,6 +9222,21 @@ body.popup-mode .content-area {
|
||||
background: var(--accent); color: #fff; cursor: pointer; font-size: 12px; flex-shrink: 0; }
|
||||
.bookslm-action-apply:disabled { opacity: 0.6; cursor: default; }
|
||||
.bookslm-action-apply:not(:disabled):hover { filter: brightness(1.08); }
|
||||
/* Agent-mode toggle in the header. */
|
||||
.bookslm-header button.bookslm-btn-agent.active { color: var(--accent); background: var(--surface2); }
|
||||
/* Tool-call trace shown above an assistant answer (agent mode). */
|
||||
.bookslm-tool-trace { display: flex; flex-direction: column; gap: 2px; margin-bottom: 6px; }
|
||||
.bookslm-tool-line { font-size: 11px; color: var(--text-secondary); }
|
||||
.bookslm-tool-line.failed { color: #f87171; }
|
||||
/* Mutation confirmation card (two-step propose/apply). */
|
||||
.bookslm-confirm { flex-wrap: wrap; }
|
||||
.bookslm-confirm-diff { flex-basis: 100%; width: 100%; margin-top: 4px; }
|
||||
.bookslm-diff-title { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; }
|
||||
.bookslm-diff { background: rgba(0,0,0,0.25); border-radius: 6px; padding: 8px;
|
||||
overflow-x: auto; font-size: 12px; line-height: 1.4; max-height: 240px; margin: 0; }
|
||||
.bookslm-diff-add { color: #4ade80; display: block; }
|
||||
.bookslm-diff-del { color: #f87171; display: block; }
|
||||
.bookslm-diff-ctx { color: var(--text-secondary); display: block; }
|
||||
.bookslm-suggestions { display: flex; flex-direction: column; gap: 6px; padding: 8px 16px 0; }
|
||||
.bookslm-suggestion { padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border);
|
||||
background: var(--surface); color: var(--text-secondary); cursor: pointer; font-size: 13px; text-align: left; }
|
||||
|
||||
Reference in New Issue
Block a user