feat: advance local-first workspace checklist

- add admin access-policy UI and local access control surfaces

- add local markdown conflict resolution UI and smoke coverage

- add ACP local agent changed-files audit scaffold and read-only write guard

- document current P0-P2 checklist progress and verification evidence
This commit is contained in:
lix-2026
2026-05-19 08:07:17 +08:00
parent a2cb1338c8
commit 68d321e297
36 changed files with 8643 additions and 235 deletions
+356 -42
View File
@@ -7,10 +7,11 @@ const SIDEBAR_TREE_JS: &str = r##"
if (window.__mnoteSidebarTreeRuntimeStarted) return;
window.__mnoteSidebarTreeRuntimeStarted = true;
var PAGE_AI_SESSION_STORAGE_VERSION = 3;
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
var MNOTE_RECENT_LOCAL_ROOTS_KEY = 'mnote.localFolder.recentRoots';
var MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:';
var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
var mnoteNavigationInFlight = '';
@@ -39,7 +40,7 @@ const SIDEBAR_TREE_JS: &str = r##"
pageAiPage: 'chat',
pageAiRunStatus: 'idle',
pageAiCurrentRunId: '',
pageAiAcpRuntime: '',
pageAiAcpRuntime: 'reasonix',
pageAiAcpRuntimes: [],
pageAiQueueLength: 0,
pageAiQueuedItems: [],
@@ -618,7 +619,22 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function currentSourceKind() {
return (new URLSearchParams(window.location.search).get('sourceKind') || 'convex_workspace').trim() || 'convex_workspace';
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('sourceKind') || '').trim();
if (fromUrl) return fromUrl;
if (
window.location.pathname === '/' &&
!(params.get('workspaceId') || '').trim() &&
!(params.get('rootUri') || '').trim() &&
!(params.get('pageId') || '').trim()
) {
return 'local_folder';
}
return 'convex_workspace';
}
function currentRootUri() {
return (new URLSearchParams(window.location.search).get('rootUri') || '').trim();
}
function rememberCloudWorkspaceId(workspaceId) {
@@ -680,9 +696,22 @@ const SIDEBAR_TREE_JS: &str = r##"
return payload;
}
function currentActorStorageId() {
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-actor-id') || '').trim() : '';
if (fromBody && fromBody !== 'anonymous') return fromBody;
return '';
}
function recentLocalRootsStorageKey() {
var actorId = currentActorStorageId();
return actorId ? MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX + encodeURIComponent(actorId) : '';
}
function readRecentLocalRoots() {
try {
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_RECENT_LOCAL_ROOTS_KEY) : '';
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return [];
var raw = window.localStorage ? window.localStorage.getItem(storageKey) : '';
var parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter(function(value) {
return typeof value === 'string' && value.trim();
@@ -695,9 +724,11 @@ const SIDEBAR_TREE_JS: &str = r##"
function rememberLocalRoot(rootUri) {
try {
if (!window.localStorage) return;
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return;
var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
roots.unshift(rootUri);
window.localStorage.setItem(MNOTE_RECENT_LOCAL_ROOTS_KEY, JSON.stringify(roots.slice(0, 10)));
window.localStorage.setItem(storageKey, JSON.stringify(roots.slice(0, 10)));
} catch (_) {}
}
@@ -1036,6 +1067,47 @@ const SIDEBAR_TREE_JS: &str = r##"
openLocalFolderDialog('');
}
async function createDefaultLocalWorkspace(trigger) {
setCommandPending(trigger, true);
try {
var response = await fetch('/api/local-folder/workspaces/default', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error((payload && payload.message) || ('local_workspace_create_failed_' + response.status));
}
var workspace = payload && payload.workspace || {};
var rootUri = String(workspace.rootUri || '').trim();
if (!rootUri) throw new Error('local_workspace_create_missing_root_uri');
openLocalFolderRoot(rootUri);
} catch (error) {
var status = document.querySelector('[data-testid="mnote-local-folder-status"]');
if (status instanceof HTMLElement) {
status.textContent = error && error.message ? error.message : String(error);
} else {
openLocalFolderDialog(error && error.message ? error.message : String(error));
}
} finally {
setCommandPending(trigger, false);
}
}
function autoOpenRecentLocalRootOnHome() {
if (window.location.pathname !== '/') return false;
var params = new URLSearchParams(window.location.search);
if ((params.get('sourceKind') || '').trim()) return false;
if ((params.get('rootUri') || '').trim()) return false;
if ((params.get('workspaceId') || '').trim()) return false;
if ((params.get('pageId') || '').trim()) return false;
var recent = readRecentLocalRoots();
if (!recent.length) return false;
openLocalFolderRoot(recent[0]);
return true;
}
function closeWorkspaceSourceMenu() {
var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
@@ -1109,6 +1181,8 @@ const SIDEBAR_TREE_JS: &str = r##"
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
}
autoOpenRecentLocalRootOnHome();
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
@@ -1970,6 +2044,21 @@ const SIDEBAR_TREE_JS: &str = r##"
return assetId.indexOf('mindmap_') === 0 || assetId.indexOf('mindmap-') === 0;
}
function localFilePathFromAssetId(assetId) {
var value = String(assetId || '').trim();
return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : '';
}
function buildLocalFileOpenUrl(relativePath, download) {
var rootUri = currentRootUri();
if (!rootUri || !relativePath) return '';
var url = new URL('/api/local-folder/files/open', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('path', relativePath);
if (download) url.searchParams.set('download', 'true');
return url.toString();
}
function readFileTreeObjectIdentity(row) {
if (!row) return null;
var raw = row.getAttribute('data-object-identity') || '';
@@ -1999,6 +2088,12 @@ const SIDEBAR_TREE_JS: &str = r##"
async function openConvexAssetFromFileTree(detail) {
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
var localFilePath = localFilePathFromAssetId(assetId);
if (localFilePath) {
var localFileUrl = buildLocalFileOpenUrl(localFilePath, false);
if (localFileUrl) window.open(localFileUrl, '_blank', 'noopener,noreferrer');
return;
}
var documentId = String(detail && detail.documentId || '').trim();
if (isMindmapAssetDetail(detail) && documentId) {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-object-shell');
@@ -2133,13 +2228,18 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function uploadedAssetUrl(asset) {
return String(asset && (asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
}
function uploadedAssetType(asset) {
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
}
function isLocalUploadedAsset(asset) {
var id = String(asset && asset.id || '').trim();
return String(asset && asset.sourceKind || '').trim() === 'local_folder' || id.indexOf('local:asset:') === 0;
}
function uploadedAssetExtension(asset) {
var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
return match ? match[1] : '';
@@ -2224,6 +2324,7 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function buildOnlyOfficeAssetOpenUrl(asset, userId) {
if (isLocalUploadedAsset(asset)) return '';
var title = uploadedAssetTitle(asset);
var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type);
if (!fileType) return '';
@@ -2591,8 +2692,9 @@ const SIDEBAR_TREE_JS: &str = r##"
if (type === 'image' && url) {
return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
}
var isLocalAsset = isLocalUploadedAsset(asset);
var userId = '';
var onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
var onlyOfficeUrl = isLocalAsset ? '' : buildOnlyOfficeAssetOpenUrl(asset, userId);
if (onlyOfficeUrl && assetId) {
userId = await fetchCurrentOnlyOfficeUserId();
onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
@@ -2647,6 +2749,35 @@ const SIDEBAR_TREE_JS: &str = r##"
}
async function uploadFileToMediaAsset(file, plan, options) {
if (currentSourceKind() === 'local_folder') {
var rootUri = (new URLSearchParams(window.location.search).get('rootUri') || '').trim();
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
if (!rootUri || !documentId) {
throw new Error(' Markdown rootUri documentId');
}
var localForm = new FormData();
localForm.append('file', file);
localForm.append('rootUri', rootUri);
localForm.append('documentId', documentId);
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment');
var localResponse = await fetch('/api/local-folder/assets/upload', {
method: 'POST',
credentials: 'include',
body: localForm
});
var localPayload = await localResponse.json().catch(function() { return null; });
if (!localResponse.ok || !localPayload || !localPayload.asset) {
throw new Error(localPayload && localPayload.error ? localPayload.error : '');
}
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(localPayload.asset);
}
void refreshLocalFolderSidebarSnapshot();
window.dispatchEvent(new CustomEvent('wolai:local-assets-changed', {
detail: { docId: documentId, asset: localPayload.asset, assetIds: [localPayload.asset.id] }
}));
return localPayload.asset;
}
var form = new FormData();
form.append('file', file);
form.append('workspaceId', plan.workspaceId);
@@ -3951,6 +4082,8 @@ const SIDEBAR_TREE_JS: &str = r##"
params.set('workspaceId', resolveWorkspaceId(document.body));
params.set('documentId', currentDocumentId());
params.set('profile', pageAiRunProfile());
params.set('sourceKind', currentSourceKind());
if (currentRootUri()) params.set('rootUri', currentRootUri());
Object.keys(extra || {}).forEach(function(key) {
var value = extra[key];
if (value !== undefined && value !== null && String(value).trim() !== '') {
@@ -3966,7 +4099,7 @@ const SIDEBAR_TREE_JS: &str = r##"
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
title: title || '',
profile: pageAiCurrentProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || '',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
createdAt: now,
updatedAt: now,
source: 'local',
@@ -4093,14 +4226,20 @@ const SIDEBAR_TREE_JS: &str = r##"
return (Array.isArray(sessions) ? sessions : [])
.slice(0, 20)
.map(function(session) {
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
return {
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '',
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: String(session && session.acpRuntime || '').trim(),
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
createdAt: pageAiTimestamp(session && session.createdAt),
updatedAt: pageAiTimestamp(session && session.updatedAt),
source: String(session && session.source || 'local').trim() || 'local',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
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,
@@ -4120,6 +4259,10 @@ const SIDEBAR_TREE_JS: &str = r##"
if (!sessionId) return null;
var title = String(row.title || payload.title || payload.message || '').trim();
if (title.length > 28) title = title.slice(0, 28) + '…';
var persistence = String(row.persistence || payload.persistence || '').trim();
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
return {
id: sessionId,
title: title || '',
@@ -4127,7 +4270,11 @@ const SIDEBAR_TREE_JS: &str = r##"
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',
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
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,
@@ -4150,6 +4297,16 @@ const SIDEBAR_TREE_JS: &str = r##"
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
}
function pageAiSessionStorageLabel(session) {
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
if (storage === 'local_shared') return '';
if (storage === 'local_private') return '';
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '';
if (persistence === 'local_ai_session_jsonl') return '';
return String(session && session.source || '').trim() === 'acp' ? '' : '';
}
function pageAiLoadSessions() {
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
try {
@@ -4159,14 +4316,15 @@ const SIDEBAR_TREE_JS: &str = r##"
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions);
if (activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
var storageVersion = Number(parsed && parsed.version || 0);
if (storageVersion >= PAGE_AI_SESSION_STORAGE_VERSION && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
if (activeProfile) pageAiSetActiveProfile(activeProfile);
if (sessions.length) {
pageUiState.pageAiSessions = sessions;
var activeSession = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = activeSession.id;
if (activeSession.profile) pageAiSetActiveProfile(activeSession.profile);
if (activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
if (!pageUiState.pageAiAcpRuntime && activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(activeSession.messages) ? activeSession.messages.slice() : [];
return;
}
@@ -4201,7 +4359,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var active = pageAiCurrentSession();
if (active) {
if (active.profile) pageAiSetActiveProfile(active.profile);
if (active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
}
pageUiState.pageAiSessionError = '';
@@ -4319,10 +4477,10 @@ const SIDEBAR_TREE_JS: &str = r##"
try {
pageAiSyncCurrentSessionMessages();
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
version: 2,
version: PAGE_AI_SESSION_STORAGE_VERSION,
activeSessionId: pageUiState.pageAiActiveSessionId,
activeProfileName: pageAiCurrentProfile(),
activeAcpRuntime: pageUiState.pageAiAcpRuntime || '',
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
}));
} catch (_) {}
@@ -4338,6 +4496,8 @@ const SIDEBAR_TREE_JS: &str = r##"
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
traceId: 'page-ai-' + Date.now().toString(36),
profile: pageAiCurrentProfile(),
title: current && current.title ? current.title : ''
@@ -4351,7 +4511,11 @@ const SIDEBAR_TREE_JS: &str = r##"
id: String(payload.sessionId || '').trim(),
title: String(payload.title || ''),
profile: String(payload.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: pageUiState.pageAiAcpRuntime || '',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
persistence: String(payload.persistence || '').trim(),
sessionStorage: String(payload.sessionStorage || '').trim(),
permissionLevel: String(payload.permissionLevel || '').trim(),
shareId: String(payload.shareId || '').trim(),
createdAt: Date.now(),
updatedAt: Date.now(),
messages: pageUiState.pageAiMessages.slice()
@@ -4411,7 +4575,7 @@ const SIDEBAR_TREE_JS: &str = r##"
if (!session) return;
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
session.profile = pageAiCurrentProfile();
session.acpRuntime = pageUiState.pageAiAcpRuntime || '';
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
session.updatedAt = Date.now();
}
@@ -4518,6 +4682,36 @@ const SIDEBAR_TREE_JS: &str = r##"
return Array.isArray(value) ? value : [];
}
function pageAiDefaultAcpRuntimes() {
return [
{
name: 'reasonix',
title: 'ACP · Reasonix',
description: ' ACP ReasonixDeepSeek ',
model: 'deepseek-chat',
preset: 'auto'
},
{
name: 'hermes',
title: 'ACP · Hermes',
description: ' ACP Hermes agent runtime'
}
];
}
function pageAiNormalizeAcpRuntimes(runtimes) {
var byName = {};
pageAiDefaultAcpRuntimes().forEach(function(runtime) {
byName[runtime.name] = Object.assign({}, runtime);
});
pageAiNormalizeArray(runtimes).forEach(function(runtime) {
var name = String(runtime && runtime.name || '').trim();
if (name !== 'reasonix' && name !== 'hermes') return;
byName[name] = Object.assign({}, byName[name] || {}, runtime, { name: name });
});
return ['reasonix', 'hermes'].map(function(name) { return byName[name]; }).filter(Boolean);
}
function pageAiUnwrapUpstream(payload) {
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
return payload || null;
@@ -4540,7 +4734,7 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function pageAiRunProfile() {
return String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
return String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
}
function pageAiMnoteToolModel() {
@@ -4705,6 +4899,15 @@ const SIDEBAR_TREE_JS: &str = r##"
return String(name || '').trim().replace(/_/g, '.');
}
function pageAiFormatChangedFiles(files) {
return pageAiNormalizeArray(files).map(function(file) {
var path = String(file && file.path || '').trim();
var changeType = String(file && file.changeType || file.change_type || 'modified').trim();
var summary = String(file && file.summary || '').trim();
return [changeType, path, summary].filter(Boolean).join(' · ');
}).filter(Boolean).join('\n');
}
function pageAiToolEventDeepFindString(value, keys, depth) {
if (!value || typeof value !== 'object' || depth > 5) return '';
for (var index = 0; index < keys.length; index += 1) {
@@ -5005,7 +5208,7 @@ const SIDEBAR_TREE_JS: &str = r##"
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || '',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
reason: 'page_ai_user_stop'
})
});
@@ -5063,7 +5266,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var profiles = pageAiNormalizeProfiles(payload);
pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'mnoteai', active: true }];
var acpRuntimes = Array.isArray(payload.acpRuntimes) ? payload.acpRuntimes : [];
pageUiState.pageAiAcpRuntimes = acpRuntimes;
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(acpRuntimes);
var current = pageAiCurrentProfile();
var hasCurrent = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === current; });
var hasMnoteAi = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === 'mnoteai'; });
@@ -5074,6 +5277,7 @@ const SIDEBAR_TREE_JS: &str = r##"
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'mnoteai', active: true }];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(pageUiState.pageAiAcpRuntimes);
pageAiSetActiveProfile(pageAiCurrentProfile());
}
renderPageAiProviderButtons();
@@ -5156,7 +5360,7 @@ const SIDEBAR_TREE_JS: &str = r##"
async function pageAiLoadSkills() {
try {
var runtime = String(pageUiState.pageAiAcpRuntime || '').trim();
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
var params = runtime === 'reasonix'
? 'runtime=reasonix'
: 'profile=' + encodeURIComponent(pageAiCurrentProfile());
@@ -5178,7 +5382,7 @@ const SIDEBAR_TREE_JS: &str = r##"
async function pageAiToggleSkill(skillName, enabled) {
var name = String(skillName || '').trim();
if (!name) return;
if (String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix') return;
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return;
var previous = null;
pageAiSkillListEntries().forEach(function(skill) {
if (skill.name === name && previous == null) previous = skill.enabled !== false;
@@ -5275,16 +5479,15 @@ const SIDEBAR_TREE_JS: &str = r##"
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeProfile = pageAiRunProfile();
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || '');
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
// Populate ACP runtime dropdown
var acpSelect = drawer.querySelector('[data-page-ai-acp-runtime]');
if (acpSelect instanceof HTMLSelectElement) {
var runtimes = pageUiState.pageAiAcpRuntimes.length ? pageUiState.pageAiAcpRuntimes : [];
acpSelect.innerHTML = '<option value=""> (Hermes HTTP)</option>' +
runtimes.map(function(rt) {
var runtimes = pageUiState.pageAiAcpRuntimes.length ? pageUiState.pageAiAcpRuntimes : pageAiNormalizeAcpRuntimes([]);
acpSelect.innerHTML = runtimes.map(function(rt) {
return '<option value="' + escapeHtml(rt.name || '') + '"' + ((rt.name || '') === pageUiState.pageAiAcpRuntime ? ' selected' : '') + '>' + escapeHtml(rt.title || rt.name) + '</option>';
}).join('');
acpSelect.value = pageUiState.pageAiAcpRuntime || '';
acpSelect.value = pageUiState.pageAiAcpRuntime || 'reasonix';
}
// Show/hide Hermes-specific profile select
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
@@ -5366,7 +5569,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var session = pageAiCurrentSession();
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
sessionNode.textContent = session && session.id
? [String(session.title || ''), usageText].filter(Boolean).join(' · ')
? [String(session.title || ''), pageAiSessionStorageLabel(session), usageText].filter(Boolean).join(' · ')
: ' Hermes session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
@@ -5438,7 +5641,7 @@ const SIDEBAR_TREE_JS: &str = r##"
if (skillList instanceof HTMLElement) {
var skills = pageAiFilteredSkillEntries();
if (!skills.length) {
var emptyText = String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix'
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
? ' Reasonix skill'
: ' Hermes skill';
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
@@ -5447,7 +5650,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
var description = String(skill.description || '').trim();
var hasDescription = description && description !== '---' && description !== '';
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || '').trim() !== 'reasonix';
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
return '' +
'<div class="wolai-page-ai-skill-row">' +
'<div class="wolai-page-ai-skill-copy">' +
@@ -5618,9 +5821,7 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-page-ai-settings-grid">' +
'<label class="wolai-page-ai-profile-select">' +
'<span>ACP</span>' +
'<select data-page-ai-acp-runtime>' +
'<option value=""> (Hermes HTTP)</option>' +
'</select>' +
'<select data-page-ai-acp-runtime></select>' +
'</label>' +
'<label class="wolai-page-ai-profile-select" data-page-ai-hermes-profile>' +
'<span>agent / profile</span>' +
@@ -5756,7 +5957,7 @@ const SIDEBAR_TREE_JS: &str = r##"
: (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(' · ');
var meta = [pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
return '' +
'<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) + '">' +
@@ -5784,6 +5985,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var detailRows = [
item.argsSummary ? '<div class="wolai-page-ai-tool-meta"> ' + escapeHtml(item.argsSummary) + '</div>' : '',
item.resultSummary ? '<div class="wolai-page-ai-tool-meta"> ' + escapeHtml(item.resultSummary) + '</div>' : '',
item.changedFiles && item.changedFiles.length ? '<pre class="wolai-page-ai-tool-meta wolai-page-ai-changed-files">' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '</pre>' : '',
(item.traceId || item.auditId) ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '</div>' : ''
].filter(Boolean).join('');
return '' +
@@ -5975,6 +6177,7 @@ const SIDEBAR_TREE_JS: &str = r##"
}
async function pageAiTryBlockEditWorkflow(prompt, scopedContext) {
if (currentSourceKind() === 'local_folder') return false;
if (!pageAiLooksLikeBlockEdit(prompt)) return false;
var runId = 'page-ai-fast-' + Date.now().toString(36);
var traceId = 'page-ai-fast-' + Date.now().toString(36);
@@ -5985,11 +6188,13 @@ const SIDEBAR_TREE_JS: &str = r##"
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sessionId: pageUiState.pageAiActiveSessionId,
runId: runId,
profile: pageAiCurrentProfile(),
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
runId: runId,
profile: pageAiCurrentProfile(),
model: pageAiMnoteToolModel(),
message: prompt,
pageContext: scopedContext.pageContext,
@@ -6083,9 +6288,11 @@ const SIDEBAR_TREE_JS: &str = r##"
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || '',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
contextScope: pageUiState.pageAiContextScope,
message: prompt,
model: pageAiMnoteToolModel(),
@@ -6184,6 +6391,23 @@ const SIDEBAR_TREE_JS: &str = r##"
var completedSession = pageAiCurrentSession();
if (completedSession) completedSession.usage = completed.usage;
}
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles);
if (changedFiles.length) {
pageUiState.pageAiMessages.push({
role: 'tool',
content: 'agent.changed_files',
toolCallId: runId + ':agent.changed_files',
toolName: 'agent.changed_files',
toolKind: 'audit',
status: 'completed',
argsSummary: String(agentAudit.rootUri || ''),
resultSummary: String(agentAudit.diffSummary || changedFiles.length + ' changed file(s)'),
changedFiles: changedFiles,
traceId: runTraceId,
auditId: String(agentAudit.eventId || '')
});
}
} catch (_) {}
pageAiSetRunStatus('completed', runId);
}
@@ -7014,6 +7238,13 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var createLocalWorkspaceTrigger = closestAction(e.target, '[data-mnote-action="create-local-workspace"]');
if (createLocalWorkspaceTrigger) {
e.preventDefault();
void createDefaultLocalWorkspace(createLocalWorkspaceTrigger);
return;
}
var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
if (createTrigger) {
e.preventDefault();
@@ -7231,7 +7462,7 @@ const SIDEBAR_TREE_JS: &str = r##"
document.addEventListener('change', function(event) {
var pageAiAcpRuntimeSelect = closestAction(event.target, '[data-page-ai-acp-runtime]');
if (pageAiAcpRuntimeSelect instanceof HTMLSelectElement) {
var next = String(pageAiAcpRuntimeSelect.value || '').trim();
var next = String(pageAiAcpRuntimeSelect.value || 'reasonix').trim() || 'reasonix';
pageUiState.pageAiAcpRuntime = next;
pageUiState.pageAiSkills = { categories: [], archived: [] };
pageUiState.pageAiSkillError = '';
@@ -7829,6 +8060,9 @@ pub fn PageLayout(
/// 顶栏当前页面标题(可选)
#[prop(optional)]
topbar_title: Option<String>,
/// 是否显示管理员授权入口
#[prop(optional)]
show_admin_access_policy: bool,
) -> impl IntoView {
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
@@ -7894,6 +8128,21 @@ pub fn PageLayout(
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
{if show_admin_access_policy {
view! {
<a
href="/admin/access-policy"
class:active={current_nav == "admin"}
title="目录授权"
aria-label="目录授权"
data-testid="mnote-admin-access-policy-entry"
>
<span class="material-symbols-outlined nav-icon" data-icon="admin_panel_settings" aria-hidden="true"></span>
</a>
}.into_any()
} else {
view! {}.into_any()
}}
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
</nav>
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
@@ -7987,6 +8236,9 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("objectIdentity: objectIdentity"));
assert!(SIDEBAR_TREE_JS.contains("workspaceId: resolveWorkspaceId(fileRow)"));
assert!(SIDEBAR_TREE_JS.contains("/api/media/sign?assetId="));
assert!(SIDEBAR_TREE_JS.contains("function localFilePathFromAssetId"));
assert!(SIDEBAR_TREE_JS.contains("/api/local-folder/files/open"));
assert!(SIDEBAR_TREE_JS.contains("local-file:"));
assert!(SIDEBAR_TREE_JS.contains("fetchCurrentOnlyOfficeUserId"));
assert!(SIDEBAR_TREE_JS.contains("/api/auth/whoami"));
assert!(SIDEBAR_TREE_JS.contains("buildOnlyOfficeOpenUrl"));
@@ -7998,8 +8250,11 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("tree-rename-input"));
assert!(SIDEBAR_TREE_JS.contains("sidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_JS.contains("pasteSidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX"));
assert!(SIDEBAR_TREE_JS.contains("function recentLocalRootsStorageKey"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-actor-id"));
assert!(SIDEBAR_TREE_JS.contains("requestOpenLocalFolder"));
assert!(SIDEBAR_TREE_JS.contains("autoOpenRecentLocalRootOnHome"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind', 'local_folder"));
assert!(SIDEBAR_TREE_JS.contains("switchToCloudWorkspace"));
assert!(SIDEBAR_TREE_JS.contains("mnote-switch-cloud-workspace"));
@@ -8018,6 +8273,26 @@ mod tests {
);
}
#[test]
fn page_ai_fast_path_is_not_local_first_main_path() {
assert!(
SIDEBAR_TREE_JS.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
);
}
#[test]
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
assert!(SIDEBAR_TREE_JS.contains("function currentRootUri()"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind: currentSourceKind()"));
assert!(SIDEBAR_TREE_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_TREE_JS.contains("pageContext: scopedContext.pageContext"));
assert!(
SIDEBAR_TREE_JS.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
);
}
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
assert!(SIDEBAR_TREE_JS.contains("function pageAiLoadBackendSessions"));
@@ -8043,6 +8318,33 @@ mod tests {
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
assert!(SIDEBAR_TREE_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_TREE_JS.contains("local_private"));
assert!(SIDEBAR_TREE_JS.contains("local_shared"));
assert!(SIDEBAR_TREE_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_TREE_JS.contains("本地私有"));
assert!(SIDEBAR_TREE_JS.contains("共享会话"));
assert!(SIDEBAR_TREE_JS.contains("云端会话"));
assert!(SIDEBAR_TREE_JS.contains("sessionStorage:"));
assert!(SIDEBAR_TREE_JS.contains("permissionLevel:"));
assert!(SIDEBAR_TREE_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
assert!(SIDEBAR_TREE_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_TREE_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(SIDEBAR_TREE_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_TREE_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_TREE_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_TREE_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_TREE_JS
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
assert!(!SIDEBAR_TREE_JS.contains("默认 (Hermes HTTP)"));
}
#[test]
fn sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
@@ -8081,6 +8383,18 @@ mod tests {
assert!(!SIDEBAR_TREE_JS.contains("window.location.reload"));
}
#[test]
fn sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder() {
assert!(SIDEBAR_TREE_JS.contains("currentSourceKind() === 'local_folder'"));
assert!(SIDEBAR_TREE_JS.contains("/api/local-folder/assets/upload"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('rootUri', rootUri)"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('documentId', documentId)"));
assert!(SIDEBAR_TREE_JS.contains("isLocalUploadedAsset(asset)"));
assert!(SIDEBAR_TREE_JS.contains("if (isLocalUploadedAsset(asset)) return '';"));
assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_TREE_JS.contains("wolai:local-assets-changed"));
}
#[test]
fn sidebar_tree_runtime_contains_dev_hot_reload_client() {
assert!(SIDEBAR_TREE_JS.contains("installMnoteDevHotReload"));