chore: 保存当前架构收口与 bug 修复快照
归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
This commit is contained in:
@@ -61,13 +61,32 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageAiSkillQuery: '',
|
||||
pageAiSkillError: '',
|
||||
pageAiSessions: [],
|
||||
pageAiActiveSessionId: ''
|
||||
pageAiActiveSessionId: '',
|
||||
pageAiSessionSearchQuery: '',
|
||||
pageAiSessionSearchResults: [],
|
||||
pageAiSessionSearchTimer: 0,
|
||||
pageAiSessionError: '',
|
||||
pageAiPermissionRequests: []
|
||||
};
|
||||
|
||||
function closestAction(target, selector) {
|
||||
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
|
||||
}
|
||||
|
||||
function toggleWorkspaceSidebar(trigger) {
|
||||
var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell');
|
||||
if (!(shell instanceof HTMLElement)) return;
|
||||
var collapsed = shell.getAttribute('data-mnote-sidebar-collapsed') === 'true';
|
||||
var next = !collapsed;
|
||||
shell.setAttribute('data-mnote-sidebar-collapsed', String(next));
|
||||
document.documentElement.setAttribute('data-mnote-sidebar-collapsed', String(next));
|
||||
document.documentElement.setAttribute('data-mnote-sidebar-toggle-applied', 'true');
|
||||
if (trigger instanceof HTMLElement) {
|
||||
trigger.setAttribute('aria-pressed', String(next));
|
||||
trigger.setAttribute('title', next ? '展开侧栏' : '切换侧栏');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
@@ -227,20 +246,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
function currentPageAiContextSnapshot() {
|
||||
var aggregate = currentPageAggregate();
|
||||
var body = aggregate.body || {};
|
||||
var serverContent = body.content || null;
|
||||
var serverText = searchText(textFromUnknown(serverContent));
|
||||
var localBlocks = readLocalEditorBlocks();
|
||||
var localText = searchText(localBlocks.map(function(block) { return block.content || ''; }).join(' '));
|
||||
var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
|
||||
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
|
||||
if (localBlocks.length && localText && localText !== serverText) {
|
||||
return {
|
||||
aggregate: aggregate,
|
||||
body: Object.assign({}, body, { content: localBlocks }),
|
||||
subtree: buildPageAiLocalSubtree(localBlocks, title),
|
||||
pageSubtreeSource: 'local'
|
||||
};
|
||||
}
|
||||
return {
|
||||
aggregate: aggregate,
|
||||
body: body,
|
||||
@@ -3930,6 +3936,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return 'hermes_page_ai_session:' + currentDocumentId();
|
||||
}
|
||||
|
||||
function pageAiTimestamp(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
var parsed = Date.parse(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function pageAiBackendSessionQuery(extra) {
|
||||
var params = new URLSearchParams();
|
||||
params.set('source', 'acp');
|
||||
params.set('workspaceId', resolveWorkspaceId(document.body));
|
||||
params.set('documentId', currentDocumentId());
|
||||
params.set('profile', pageAiRunProfile());
|
||||
Object.keys(extra || {}).forEach(function(key) {
|
||||
var value = extra[key];
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function pageAiNewSession(title) {
|
||||
var now = Date.now();
|
||||
return {
|
||||
@@ -3939,10 +3969,126 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
acpRuntime: pageUiState.pageAiAcpRuntime || '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
source: 'local',
|
||||
usage: null,
|
||||
status: 'idle',
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiUsageSummary(usage) {
|
||||
if (!usage || typeof usage !== 'object') return '';
|
||||
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
|
||||
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
|
||||
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
|
||||
var parts = [];
|
||||
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
|
||||
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
|
||||
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
|
||||
return parts.join(' ') || '';
|
||||
}
|
||||
|
||||
function pageAiPermissionMessage(payload, eventType) {
|
||||
payload = payload && typeof payload === 'object' ? payload : {};
|
||||
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
|
||||
var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
|
||||
var args = payload.arguments || payload.args || payload.input || payload.params || payload;
|
||||
var decision = String(payload.decision || payload.result || '').trim();
|
||||
if (!decision && eventType === 'permission.denied') decision = 'denied';
|
||||
if (!decision && eventType === 'permission.allowed') decision = 'allowed';
|
||||
return {
|
||||
role: 'tool',
|
||||
kind: 'permission',
|
||||
permissionId: permissionId,
|
||||
toolName: toolName,
|
||||
argsSummary: pageAiPreviewValue(args),
|
||||
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
|
||||
resolved: decision === 'denied' || decision === 'allowed',
|
||||
decision: decision
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiApplyPermissionEvent(eventName, payloadText) {
|
||||
var payload = null;
|
||||
try {
|
||||
payload = JSON.parse(payloadText || 'null');
|
||||
} catch (_) {
|
||||
payload = {};
|
||||
}
|
||||
var message = pageAiPermissionMessage(payload, eventName);
|
||||
var existing = pageUiState.pageAiMessages.find(function(item) {
|
||||
return item.kind === 'permission' && item.permissionId === message.permissionId;
|
||||
});
|
||||
if (existing) {
|
||||
Object.assign(existing, message);
|
||||
} else {
|
||||
pageUiState.pageAiMessages.push(message);
|
||||
}
|
||||
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
|
||||
return item.permissionId !== message.permissionId;
|
||||
}).concat([message]).slice(-20);
|
||||
if (!message.resolved) {
|
||||
pageAiShowPermissionDialog(message);
|
||||
} else {
|
||||
pageAiHidePermissionDialog();
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiResolvePermission(permissionId, decision) {
|
||||
permissionId = String(permissionId || '').trim();
|
||||
if (!permissionId) return;
|
||||
pageUiState.pageAiMessages.forEach(function(item) {
|
||||
if (item.kind === 'permission' && item.permissionId === permissionId) {
|
||||
item.resolved = true;
|
||||
item.decision = decision;
|
||||
item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求';
|
||||
}
|
||||
});
|
||||
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
|
||||
if (dialog instanceof HTMLElement) dialog.hidden = true;
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
function pageAiHidePermissionDialog() {
|
||||
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
|
||||
if (dialog instanceof HTMLElement) dialog.hidden = true;
|
||||
}
|
||||
|
||||
function pageAiShowPermissionDialog(message) {
|
||||
if (!message || message.kind !== 'permission') return;
|
||||
if (message.resolved) {
|
||||
pageAiHidePermissionDialog();
|
||||
return;
|
||||
}
|
||||
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
|
||||
if (!(dialog instanceof HTMLElement)) {
|
||||
dialog = document.createElement('div');
|
||||
dialog.className = 'wolai-page-ai-permission-dialog';
|
||||
dialog.setAttribute('data-page-ai-permission-dialog', 'true');
|
||||
dialog.innerHTML = '' +
|
||||
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
|
||||
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
|
||||
'<div class="wolai-page-ai-tool-meta" data-page-ai-permission-args></div>' +
|
||||
'<div class="wolai-page-ai-message-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-dialog-action>允许</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-dialog-action>拒绝</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
|
||||
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
|
||||
var args = dialog.querySelector('[data-page-ai-permission-args]');
|
||||
if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || '';
|
||||
dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) {
|
||||
if (button instanceof HTMLButtonElement) {
|
||||
button.setAttribute('data-page-ai-permission-id', message.permissionId || '');
|
||||
button.disabled = Boolean(message.resolved);
|
||||
}
|
||||
});
|
||||
dialog.hidden = false;
|
||||
}
|
||||
|
||||
function pageAiNormalizeSessions(sessions) {
|
||||
return (Array.isArray(sessions) ? sessions : [])
|
||||
.slice(0, 20)
|
||||
@@ -3952,8 +4098,13 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
title: String(session && session.title || '').trim() || '新会话',
|
||||
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
|
||||
acpRuntime: String(session && session.acpRuntime || '').trim(),
|
||||
createdAt: Number(session && session.createdAt || Date.now()),
|
||||
updatedAt: Number(session && session.updatedAt || Date.now()),
|
||||
createdAt: pageAiTimestamp(session && session.createdAt),
|
||||
updatedAt: pageAiTimestamp(session && session.updatedAt),
|
||||
source: String(session && session.source || 'local').trim() || 'local',
|
||||
runId: String(session && (session.runId || session.run_id) || '').trim(),
|
||||
status: String(session && session.status || '').trim(),
|
||||
usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null,
|
||||
preview: String(session && session.preview || '').trim(),
|
||||
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : []
|
||||
};
|
||||
})
|
||||
@@ -3962,6 +4113,43 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiNormalizeBackendSessionRow(row) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
|
||||
var sessionId = String(row.sessionId || row.session_id || '').trim();
|
||||
if (!sessionId) return null;
|
||||
var title = String(row.title || payload.title || payload.message || '').trim();
|
||||
if (title.length > 28) title = title.slice(0, 28) + '…';
|
||||
return {
|
||||
id: sessionId,
|
||||
title: title || '当前页问答',
|
||||
profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default',
|
||||
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
|
||||
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
|
||||
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
|
||||
source: 'acp',
|
||||
runId: String(row.runId || row.run_id || '').trim(),
|
||||
status: String(row.status || (row.runtime && row.runtime.status) || '').trim(),
|
||||
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
|
||||
preview: String(payload.message || row.snippet || '').trim(),
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
function pageAiMergeSessions(localSessions, backendSessions) {
|
||||
var byId = {};
|
||||
pageAiNormalizeSessions(localSessions).forEach(function(session) {
|
||||
byId[session.id] = session;
|
||||
});
|
||||
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
|
||||
var existing = byId[session.id];
|
||||
byId[session.id] = Object.assign({}, existing || {}, session, {
|
||||
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
|
||||
});
|
||||
});
|
||||
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
|
||||
}
|
||||
|
||||
function pageAiLoadSessions() {
|
||||
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
|
||||
try {
|
||||
@@ -3996,6 +4184,135 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageUiState.pageAiMessages = [];
|
||||
}
|
||||
|
||||
async function pageAiLoadBackendSessions() {
|
||||
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
|
||||
}
|
||||
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
|
||||
if (!backendSessions.length) return [];
|
||||
pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions);
|
||||
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
|
||||
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
|
||||
}
|
||||
var active = pageAiCurrentSession();
|
||||
if (active) {
|
||||
if (active.profile) pageAiSetActiveProfile(active.profile);
|
||||
if (active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
|
||||
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
|
||||
}
|
||||
pageUiState.pageAiSessionError = '';
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return backendSessions;
|
||||
}
|
||||
|
||||
function pageAiMessageFromRuntimeEvent(event) {
|
||||
if (!event || typeof event !== 'object') return null;
|
||||
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
|
||||
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
|
||||
if (eventType === 'message.delta') {
|
||||
var delta = String(payload.delta || payload.text || payload.output_text || '').trim();
|
||||
return delta ? { role: 'assistant', content: delta } : null;
|
||||
}
|
||||
if (eventType === 'thought.delta') {
|
||||
var thought = String(payload.delta || payload.text || '').trim();
|
||||
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
|
||||
}
|
||||
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
|
||||
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
|
||||
return {
|
||||
role: 'tool',
|
||||
content: toolName,
|
||||
toolName: toolName,
|
||||
toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''),
|
||||
toolKind: String(payload.kind || ''),
|
||||
status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'),
|
||||
argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input),
|
||||
resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error),
|
||||
traceId: String(payload.traceId || payload.trace_id || ''),
|
||||
auditId: String(payload.auditId || payload.audit_id || '')
|
||||
};
|
||||
}
|
||||
if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') {
|
||||
return pageAiPermissionMessage(payload, eventType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function pageAiApplyBackendSessionDetail(payload) {
|
||||
var sessionPayload = payload && payload.session ? payload.session : {};
|
||||
var runs = pageAiNormalizeArray(sessionPayload.runs);
|
||||
var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null;
|
||||
var events = pageAiNormalizeArray(payload && payload.events);
|
||||
var messages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) {
|
||||
return {
|
||||
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
|
||||
content: String(message.content || '')
|
||||
};
|
||||
}).filter(function(message) { return message.content; });
|
||||
events.forEach(function(event) {
|
||||
var message = pageAiMessageFromRuntimeEvent(event);
|
||||
if (message) messages.push(message);
|
||||
});
|
||||
var current = pageAiCurrentSession();
|
||||
if (latest && current) {
|
||||
Object.assign(current, latest);
|
||||
}
|
||||
if (current) {
|
||||
current.messages = messages.slice(-300);
|
||||
current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now());
|
||||
if (latest && latest.usage) current.usage = latest.usage;
|
||||
}
|
||||
pageAiApplyRuntimeState(payload && payload.runtime);
|
||||
pageUiState.pageAiMessages = messages.slice(-300);
|
||||
pageAiPersistSessions();
|
||||
}
|
||||
|
||||
async function pageAiLoadBackendSessionDetail(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return null;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status));
|
||||
}
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function pageAiSearchBackendSessions(query) {
|
||||
var q = String(query || '').trim();
|
||||
pageUiState.pageAiSessionSearchQuery = q;
|
||||
if (!q) {
|
||||
pageUiState.pageAiSessionSearchResults = [];
|
||||
renderPageAiConversation();
|
||||
return [];
|
||||
}
|
||||
var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status));
|
||||
}
|
||||
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) {
|
||||
var normalized = pageAiNormalizeBackendSessionRow(row) || {};
|
||||
normalized.snippet = String(row && row.snippet || normalized.preview || '').trim();
|
||||
return normalized;
|
||||
}).filter(function(row) { return row.id; });
|
||||
renderPageAiConversation();
|
||||
return pageUiState.pageAiSessionSearchResults;
|
||||
}
|
||||
|
||||
function pageAiPersistSessions() {
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
|
||||
@@ -4049,12 +4366,18 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
async function pageAiRestoreHermesSession() {
|
||||
var current = pageAiCurrentSession();
|
||||
if (!current || !String(current.id || '').startsWith('mnote_')) return;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume', {
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'POST',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
if (!response.ok) return;
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (payload && payload.persistence === 'convex_acp_runtime_store') {
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
|
||||
var messages = session && Array.isArray(session.messages) ? session.messages : [];
|
||||
pageAiApplyRuntimeState(payload && payload.runtime);
|
||||
@@ -4101,6 +4424,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageAiPersistSessions();
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) {
|
||||
void pageAiLoadBackendSessionDetail(session.id).catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiStartNewSession() {
|
||||
@@ -4114,6 +4443,71 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiRenameBackendSession(sessionId) {
|
||||
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
||||
if (!session) return;
|
||||
var title = window.prompt('重命名 AI 会话', session.title || '当前页问答');
|
||||
if (title === null) return;
|
||||
title = String(title || '').trim();
|
||||
if (!title) return;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
|
||||
body: JSON.stringify({ title: title })
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status));
|
||||
}
|
||||
session.title = String((payload.result && payload.result.title) || title);
|
||||
session.updatedAt = Date.now();
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiDeleteBackendSession(sessionId) {
|
||||
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
|
||||
if (!session) return;
|
||||
if (!window.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return;
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'DELETE',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
|
||||
}
|
||||
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; });
|
||||
if (pageUiState.pageAiActiveSessionId === sessionId) {
|
||||
var next = pageUiState.pageAiSessions[0] || pageAiNewSession();
|
||||
if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next];
|
||||
pageUiState.pageAiActiveSessionId = next.id;
|
||||
pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : [];
|
||||
}
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
async function pageAiResumeBackendSession(sessionId) {
|
||||
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
|
||||
if (!sessionId) return;
|
||||
pageAiSetActiveSession(sessionId);
|
||||
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
|
||||
method: 'POST',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status));
|
||||
}
|
||||
pageAiApplyBackendSessionDetail(payload);
|
||||
pageUiState.pageAiPage = 'chat';
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiProviderLabel(provider) {
|
||||
if (provider === 'codex') return 'Codex';
|
||||
if (provider === 'claudecode') return 'ClaudeCode';
|
||||
@@ -4970,7 +5364,10 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
|
||||
if (sessionNode instanceof HTMLElement) {
|
||||
var session = pageAiCurrentSession();
|
||||
sessionNode.textContent = session && session.id ? String(session.title || '当前页问答') : '等待 Hermes session';
|
||||
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
|
||||
sessionNode.textContent = session && session.id
|
||||
? [String(session.title || '当前页问答'), usageText].filter(Boolean).join(' · ')
|
||||
: '等待 Hermes session';
|
||||
}
|
||||
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
|
||||
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
|
||||
@@ -5024,6 +5421,15 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
skillError.textContent = pageUiState.pageAiSkillError || '';
|
||||
skillError.hidden = !pageUiState.pageAiSkillError;
|
||||
}
|
||||
var sessionError = drawer.querySelector('[data-page-ai-session-error]');
|
||||
if (sessionError instanceof HTMLElement) {
|
||||
sessionError.textContent = pageUiState.pageAiSessionError || '';
|
||||
sessionError.hidden = !pageUiState.pageAiSessionError;
|
||||
}
|
||||
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
|
||||
if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) {
|
||||
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
|
||||
}
|
||||
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
|
||||
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
|
||||
skillSearch.value = pageUiState.pageAiSkillQuery;
|
||||
@@ -5290,6 +5696,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
'<div class="wolai-page-ai-settings-head">' +
|
||||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||||
'</div>' +
|
||||
'<label class="wolai-page-ai-skill-search">' +
|
||||
'<span>搜索会话</span>' +
|
||||
'<input type="search" data-page-ai-session-search placeholder="搜索后端 AI 会话…" />' +
|
||||
'</label>' +
|
||||
'<div class="wolai-page-ai-inline-error" data-page-ai-session-error hidden></div>' +
|
||||
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
||||
'</section>' +
|
||||
'</div>' +
|
||||
@@ -5332,20 +5743,33 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
|
||||
if (!(conversation instanceof HTMLElement)) return;
|
||||
if (pageUiState.pageAiPage === 'history') {
|
||||
if (!pageUiState.pageAiSessions.length) {
|
||||
var historyRows = pageUiState.pageAiSessionSearchResults.length
|
||||
? pageUiState.pageAiSessionSearchResults
|
||||
: pageUiState.pageAiSessions;
|
||||
if (!historyRows.length) {
|
||||
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
|
||||
return;
|
||||
}
|
||||
conversation.innerHTML = pageUiState.pageAiSessions.map(function(session) {
|
||||
conversation.innerHTML = historyRows.map(function(session) {
|
||||
var preview = Array.isArray(session.messages) && session.messages.length
|
||||
? session.messages.slice(-1)[0].content
|
||||
: '暂无消息';
|
||||
: (session.snippet || session.preview || '暂无消息');
|
||||
var active = session.id === pageUiState.pageAiActiveSessionId;
|
||||
var usage = pageAiUsageSummary(session.usage);
|
||||
var meta = [session.source === 'acp' ? 'Convex' : '本地', session.status, usage].filter(Boolean).join(' · ');
|
||||
return '' +
|
||||
'<button type="button" class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session="' + escapeHtml(session.id) + '">' +
|
||||
'<div class="wolai-page-ai-message-role">会话</div>' +
|
||||
'<div class="wolai-page-ai-message-text"><strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' + escapeHtml(preview) + '</div>' +
|
||||
'</button>';
|
||||
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '">' +
|
||||
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
|
||||
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
|
||||
'<span>' + escapeHtml(preview) + '</span>' +
|
||||
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
|
||||
'</button>' +
|
||||
'<div class="wolai-page-ai-message-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return;
|
||||
}
|
||||
@@ -5376,6 +5800,30 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
if (item.kind === 'thought') {
|
||||
return '' +
|
||||
'<details class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-thought-delta="true">' +
|
||||
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.7;font-size:0.85em">思考过程</summary>' +
|
||||
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.6;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' + escapeHtml(item.content || '') + '</div>' +
|
||||
'</details>';
|
||||
}
|
||||
if (item.kind === 'permission') {
|
||||
var permissionActions = item.resolved ? '' : (
|
||||
'<div class="wolai-page-ai-message-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">允许</button>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">拒绝</button>' +
|
||||
'</div>'
|
||||
);
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
|
||||
'<div class="wolai-page-ai-message-role">权限</div>' +
|
||||
'<div class="wolai-page-ai-message-text">' +
|
||||
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
|
||||
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
|
||||
permissionActions +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '"' + streamingAttr + '>' +
|
||||
@@ -5401,7 +5849,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageAiLoadProfileMemory(),
|
||||
pageAiLoadSkills(),
|
||||
pageAiLoadTools(),
|
||||
pageAiLoadGatewayHealth()
|
||||
pageAiLoadGatewayHealth(),
|
||||
pageAiLoadBackendSessions()
|
||||
]).then(function() {
|
||||
renderPageAiControls();
|
||||
}).catch(function() {}).then(function() {
|
||||
@@ -5687,10 +6136,54 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
if (pageUiState.pageAiStoppedRunIds[runId]) return;
|
||||
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
|
||||
}
|
||||
if (eventName === 'thought.delta') {
|
||||
try {
|
||||
var thoughtPayload = JSON.parse(payloadText || 'null');
|
||||
var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || '');
|
||||
if (thoughtText) {
|
||||
var msgs = pageUiState.pageAiMessages;
|
||||
var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null;
|
||||
if (lastThought) {
|
||||
lastThought.content += thoughtText;
|
||||
} else {
|
||||
msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText });
|
||||
}
|
||||
pageAiSyncCurrentSessionMessages();
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eventName === 'usage.updated') {
|
||||
try {
|
||||
var usagePayload = JSON.parse(payloadText || 'null') || {};
|
||||
var sessionForUsage = pageAiCurrentSession();
|
||||
if (sessionForUsage) {
|
||||
sessionForUsage.usage = {
|
||||
source: 'usage_update',
|
||||
used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0),
|
||||
size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0)
|
||||
};
|
||||
pageAiPersistSessions();
|
||||
}
|
||||
} catch (_) {}
|
||||
renderPageAiControls();
|
||||
}
|
||||
if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') {
|
||||
pageAiApplyPermissionEvent(eventName, payloadText);
|
||||
pageAiSyncCurrentSessionMessages();
|
||||
pageAiPersistSessions();
|
||||
renderPageAiConversation();
|
||||
renderPageAiControls();
|
||||
}
|
||||
if (eventName === 'run.completed') {
|
||||
try {
|
||||
var completed = JSON.parse(payloadText || 'null');
|
||||
if (completed && completed.output) assistantText = String(completed.output || '');
|
||||
if (completed && completed.usage) {
|
||||
var completedSession = pageAiCurrentSession();
|
||||
if (completedSession) completedSession.usage = completed.usage;
|
||||
}
|
||||
} catch (_) {}
|
||||
pageAiSetRunStatus('completed', runId);
|
||||
}
|
||||
@@ -6181,6 +6674,13 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||
if (activeTreeContextMenu) closeTreeContextMenu();
|
||||
|
||||
var sidebarToggle = closestAction(e.target, '[data-mnote-action="toggle-sidebar"]');
|
||||
if (sidebarToggle) {
|
||||
e.preventDefault();
|
||||
toggleWorkspaceSidebar(sidebarToggle);
|
||||
return;
|
||||
}
|
||||
|
||||
var attachmentAction = closestAction(e.target, '[data-attachment-action]');
|
||||
if (attachmentAction) {
|
||||
e.preventDefault();
|
||||
@@ -6362,6 +6862,46 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSessionResume = closestAction(e.target, '[data-page-ai-session-resume]');
|
||||
if (pageAiSessionResume) {
|
||||
e.preventDefault();
|
||||
void pageAiResumeBackendSession(pageAiSessionResume.getAttribute('data-page-ai-session-resume') || '').catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSessionRename = closestAction(e.target, '[data-page-ai-session-rename]');
|
||||
if (pageAiSessionRename) {
|
||||
e.preventDefault();
|
||||
void pageAiRenameBackendSession(pageAiSessionRename.getAttribute('data-page-ai-session-rename') || '').catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSessionDelete = closestAction(e.target, '[data-page-ai-session-delete]');
|
||||
if (pageAiSessionDelete) {
|
||||
e.preventDefault();
|
||||
void pageAiDeleteBackendSession(pageAiSessionDelete.getAttribute('data-page-ai-session-delete') || '').catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiPermissionAction = closestAction(e.target, '[data-page-ai-permission-action]');
|
||||
if (pageAiPermissionAction) {
|
||||
e.preventDefault();
|
||||
pageAiResolvePermission(
|
||||
pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '',
|
||||
pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSession = closestAction(e.target, '[data-page-ai-session]');
|
||||
if (pageAiSession) {
|
||||
e.preventDefault();
|
||||
@@ -6396,6 +6936,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history';
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
if (pageUiState.pageAiPage === 'history') {
|
||||
void pageAiLoadBackendSessions().catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6661,6 +7207,18 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var sessionSearch = closestAction(event.target, '[data-page-ai-session-search]');
|
||||
if (sessionSearch instanceof HTMLInputElement) {
|
||||
var sessionQuery = sessionSearch.value;
|
||||
window.clearTimeout(pageUiState.pageAiSessionSearchTimer || 0);
|
||||
pageUiState.pageAiSessionSearchTimer = window.setTimeout(function() {
|
||||
void pageAiSearchBackendSessions(sessionQuery).catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
renderPageAiControls();
|
||||
});
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]');
|
||||
if (memoryEditor instanceof HTMLTextAreaElement) {
|
||||
var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || '';
|
||||
@@ -6681,6 +7239,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
void pageAiLoadProfiles();
|
||||
if (next !== 'reasonix') void pageAiLoadProfileMemory();
|
||||
void pageAiLoadSkills();
|
||||
void pageAiLoadBackendSessions().catch(function(error) {
|
||||
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
|
||||
});
|
||||
renderPageAiControls();
|
||||
renderPageAiProviderButtons();
|
||||
return;
|
||||
@@ -7343,7 +7904,7 @@ pub fn PageLayout(
|
||||
<div class="mnote-main">
|
||||
<header class="wolai-topbar" data-testid="wolai-topbar">
|
||||
<div class="wolai-topbar-left">
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏" aria-pressed="false" data-mnote-action="toggle-sidebar" data-testid="wolai-sidebar-toggle"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
||||
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
|
||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
|
||||
@@ -7445,6 +8006,59 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_JS.contains("mnote-recent-local-root"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_ai_context_uses_page_aggregate_subtree_as_single_truth() {
|
||||
assert!(
|
||||
!SIDEBAR_TREE_JS.contains("pageSubtreeSource: 'local'"),
|
||||
"页面 AI context 不能从编辑器 DOM 派生 local page subtree;必须以 Rust Page Aggregate projection 为准"
|
||||
);
|
||||
assert!(
|
||||
!SIDEBAR_TREE_JS.contains("buildPageAiLocalSubtree(localBlocks"),
|
||||
"页面 AI context 不应调用本地 page subtree builder"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_ai_uses_backend_acp_session_runtime_store() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function pageAiLoadBackendSessions"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("/api/hermes/client/sessions?"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("params.set('source', 'acp')"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function pageAiLoadBackendSessionDetail"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function pageAiSearchBackendSessions"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-rename"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-delete"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-resume"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-search"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("pageAiUsageSummary"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("usage.updated"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("thought.delta"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("permission.requested"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-permission-action=\"allow\""));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-permission-action=\"deny\""));
|
||||
assert!(SIDEBAR_TREE_JS.contains("function pageAiHidePermissionDialog"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("if (!message.resolved)"));
|
||||
assert!(
|
||||
!SIDEBAR_TREE_JS.contains("(item.resolved ? ' disabled' : '')"),
|
||||
"已决 ACP permission 事件不能继续展示假审批按钮"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector() {
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
|
||||
assert!(!SIDEBAR_TREE_JS.contains("usePreferredSidebarSnapshot"));
|
||||
assert!(!SIDEBAR_TREE_JS.contains("preferredSidebarSnapshot"));
|
||||
assert!(!SIDEBAR_TREE_JS.contains("liveSidebarTitle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_layout_sidebar_toggle_is_wired_to_shell_state() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function toggleWorkspaceSidebar"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("data-mnote-sidebar-collapsed"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("[data-mnote-action=\"toggle-sidebar\"]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_workspace_source_switch_remembers_real_cloud_workspace_before_local_folder() {
|
||||
assert!(
|
||||
@@ -7593,6 +8207,9 @@ mod tests {
|
||||
#[test]
|
||||
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-ws"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("new WebSocket(url.toString())"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("startWithSseFallback"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
|
||||
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
|
||||
|
||||
@@ -224,6 +224,11 @@ a:hover {
|
||||
background: var(--wolai-bg);
|
||||
}
|
||||
|
||||
.mnote-shell[data-mnote-sidebar-collapsed="true"] .mnote-sidebar,
|
||||
.mnote-shell[data-mnote-sidebar-collapsed="true"] .wolai-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wolai-workspace-shell {
|
||||
--wolai-bg-sidebar: #F7F7F6;
|
||||
--wolai-bg-selected: #F8E6E7;
|
||||
@@ -3376,7 +3381,6 @@ body {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message--history.is-active {
|
||||
@@ -3399,6 +3403,39 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
button.wolai-page-ai-message-text {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-message-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-permission-dialog {
|
||||
position: fixed;
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
z-index: 80;
|
||||
max-width: min(360px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.wolai-page-ai-permission-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.wolai-page-ai-tool-details {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -3670,6 +3707,7 @@ mod tests {
|
||||
assert!(MNOTE_CSS.contains(".mnote-sidebar"));
|
||||
assert!(MNOTE_CSS.contains(".mnote-main"));
|
||||
assert!(MNOTE_CSS.contains(".mnote-content"));
|
||||
assert!(MNOTE_CSS.contains("[data-mnote-sidebar-collapsed=\"true\"]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3713,7 +3751,7 @@ mod tests {
|
||||
fn mnote_css_is_reasonably_sized() {
|
||||
// 至少 2000 字符才能包含完整样式
|
||||
assert!(MNOTE_CSS.len() > 2000);
|
||||
// 当前整合了工作区壳、编辑器样式、树菜单和双 pane 布局,仍保持在单文件可审阅范围内。
|
||||
assert!(MNOTE_CSS.len() < 70000);
|
||||
// 当前整合了工作区壳、编辑器样式、树菜单、页面 AI 和双 pane 布局,仍保持在单文件可审阅范围内。
|
||||
assert!(MNOTE_CSS.len() < 90000);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user