From 4a7efd4a300371636c7f0ca7aa6871c3d30817c5 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Sat, 13 Jun 2026 22:20:01 +0800 Subject: [PATCH] feat: Page AI Reasonix desktop session alignment, settings IA cleanup, knowledge RAG hardening - ACP client/session manager: Reasonix desktop live session context - Hermes tools: knowledge_rag tool manifest and skill updates - Browser runtime: sidebar page AI permission/profile/render/session/tree modules - Routes: hermes_client, hermes_tools, knowledge_rag, web_shell - Scripts: reasonix ACP wrapper, LightRAG MCP, smoke tasks 159/558/559/561/562 - Skills: mnote-knowledge-rag and mnote-lightrag-bridge SKILL.md updates --- reasonix.toml | 82 + .../mnote-web/browser/local-upload-runtime.js | 16 +- .../sidebar-page-ai-permission-runtime.js | 250 ++- .../sidebar-page-ai-profile-runtime.js | 52 +- .../browser/sidebar-page-ai-render-runtime.js | 497 +++++- .../browser/sidebar-page-ai-runtime.js | 297 +++- .../sidebar-page-ai-session-runtime.js | 31 +- .../mnote-web/browser/sidebar-tree-runtime.js | 17 +- rust/crates/mnote-web/src/acp_client.rs | 29 +- .../mnote-web/src/acp_session_manager.rs | 322 +++- .../src/hermes_tools/knowledge_rag.rs | 209 ++- .../mnote-web/src/hermes_tools/manifest.rs | 54 + .../mnote-web/src/hermes_tools/skill.rs | 1 + .../mnote-web/src/routes/hermes_client.rs | 1215 +++++++++++++- .../mnote-web/src/routes/hermes_tools.rs | 5 + .../mnote-web/src/routes/knowledge_rag.rs | 1444 ++++++++++++++++- rust/crates/mnote-web/src/routes/mod.rs | 46 + rust/crates/mnote-web/src/routes/web_shell.rs | 18 +- rust/crates/mnote-web/src/ssr/styles.rs | 300 +++- scripts/lightrag-native-mcp.sh | 3 + scripts/mnote-lightrag-mcp-server.py | 212 ++- scripts/mnote-lightrag-mcp.sh | 3 + scripts/reasonix-acp-wrapper.mjs | 39 +- scripts/task159-auth-entry-smoke.js | 4 +- ...-ai-reasonix-live-session-context-smoke.js | 66 +- ...ai-terminal-status-reconciliation-smoke.js | 32 +- ...task561-page-ai-session-dashboard-smoke.js | 39 +- ...62-page-ai-reasonix-approval-plan-smoke.js | 304 ++++ skills/mnote-knowledge-rag/SKILL.md | 5 +- skills/mnote-lightrag-bridge/SKILL.md | 20 +- 30 files changed, 5321 insertions(+), 291 deletions(-) create mode 100644 reasonix.toml create mode 100644 scripts/task562-page-ai-reasonix-approval-plan-smoke.js diff --git a/reasonix.toml b/reasonix.toml new file mode 100644 index 00000000..95166b05 --- /dev/null +++ b/reasonix.toml @@ -0,0 +1,82 @@ +# Reasonix configuration. +# Resolution order: flag > ./reasonix.toml > ~/.config/reasonix/config.toml > built-in defaults. +# Secrets come from the environment via api_key_env; never put keys here. + +config_version = 2 # schema marker for diagnostics; old versions may ignore it +default_model = "deepseek-flash" +# language = "zh" # ui/model language; empty = auto-detect from $LANG / $REASONIX_LANG + +[agent] +# system_prompt = """...""" # omit to use the built-in prompt for this version +# system_prompt_file = "prompts/system.md" # overrides system_prompt when set +max_steps = 0 # executor tool-call rounds; 0 = no limit +planner_max_steps = 12 # planner read-only tool-call rounds; 0 = no limit +temperature = 0.0 +auto_plan = "off" # off|on; off keeps plan mode manual +# auto_plan_classifier = "deepseek-flash" # optional; only used for borderline tasks +soft_compact_ratio = 0.5 # notice only; keeps cache-first prefix intact +compact_ratio = 0.8 # try compacting when prompt reaches this fraction +compact_force_ratio = 0.9 # force compacting at this high-water mark +# planner_model = "mimo" # optional: enable two-model collaboration +# subagent_model = "deepseek-pro" # optional default for runAs=subagent skills +# subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" } # per-skill overrides +# subagent_effort = "high" # optional default effort for subagents +# subagent_efforts = { review = "max", task = "high" } # per-tool/skill effort overrides +# output_style = "explanatory" # explanatory | learning | concise | custom; empty = default + +[tools] +enabled = [] # empty = all built-in tools +bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap + +[codegraph] +enabled = true # built-in MCP server; off by default for first-run sessions +auto_install = true # fetch the runtime when CodeGraph is enabled but missing +# path = "" # empty = cache, then PATH, then a bundle beside reasonix + +[lsp] +enabled = true # language server tools; servers launch lazily when used +# [lsp.servers.go] +# command = "gopls" +# args = [] +# extensions = [".go"] + +[skills] +# paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots +# excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders +# max_depth = 3 # nested scan depth; set 1 for legacy root-only discovery +# disabled_skills = ["review"] # hide noisy or unwanted skills + +[permissions] +# Per-call gating. mode = writer fallback when no rule matches: ask|allow|deny. +# Readers always default to allow. Precedence: deny > ask > allow > fallback. +# Rules are "Tool" or "Tool(specifier)"; e.g. Bash(go test:*), Edit(src/**). +mode = "ask" +# deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode +allow = ["mcp__lightrag_native__get_health"] +# ask = ["Edit(src/**)"] # force a prompt even if otherwise allowed + +[sandbox] +# Confine tool blast radius. File-writers (write_file/edit_file/multi_edit) +# may only write under workspace_root (empty = current dir) + allow_write. +# bash = "enforce" (default) jails each command in an OS sandbox (macOS now; +# graceful fallback elsewhere); "off" disables it. network allows egress. +# workspace_root = "" # default: current working directory +# allow_write = ["/tmp"] # extra dirs writers may also modify +bash = "enforce" +network = true + +[statusline] +# A custom status line: a command whose first stdout line replaces the built-in +# data row. It receives {"model","contextUsed","contextWindow","cwd"} as JSON on stdin. +# command = "my-statusline.sh" + +# External MCP servers. type: "stdio" (default, a subprocess) | "http" | "sse". +# ${VAR} / ${VAR:-default} are expanded from the environment in command/args/env/url/headers. +# [[plugins]] +# name = "example" +# command = "reasonix-plugin-example" +# [[plugins]] # a remote server over Streamable HTTP +# name = "stripe" +# type = "http" +# url = "https://mcp.stripe.com" +# headers = { Authorization = "Bearer ${STRIPE_KEY}" } diff --git a/rust/crates/mnote-web/browser/local-upload-runtime.js b/rust/crates/mnote-web/browser/local-upload-runtime.js index c7d1c165..5ef4d450 100644 --- a/rust/crates/mnote-web/browser/local-upload-runtime.js +++ b/rust/crates/mnote-web/browser/local-upload-runtime.js @@ -356,11 +356,12 @@ function openEditorUploadFilePicker(detail, deps) { async function fetchWithTimeout(input, init, timeoutMs, label) { var controller = typeof AbortController === 'function' ? new AbortController() : null; var timer = 0; + var timeout = Math.max(1000, Number(timeoutMs) || 15000); try { if (controller) { timer = window.setTimeout(function() { controller.abort(); - }, Math.max(1000, Number(timeoutMs) || 15000)); + }, timeout); } var nextInit = Object.assign({}, init || {}); if (controller) nextInit.signal = controller.signal; @@ -375,6 +376,14 @@ async function fetchWithTimeout(input, init, timeoutMs, label) { } } +function uploadTimeoutMsForFile(file, requestedMs) { + var requested = Number(requestedMs) || 0; + var size = Number(file && file.size || 0); + if (!Number.isFinite(size) || size <= 0) return Math.max(requested, 15000); + var sizeBased = 15000 + Math.ceil(size / (1024 * 1024)) * 12000; + return Math.min(10 * 60 * 1000, Math.max(requested, 60000, sizeBased)); +} + async function uploadLocalFolderAsset(file, plan, context) { var rootUri = String(context && context.rootUri || '').trim() || currentRootUri(); var documentId = String( @@ -401,7 +410,7 @@ async function uploadLocalFolderAsset(file, plan, context) { method: 'POST', credentials: 'include', body: localForm - }, Number(context && context.timeoutMs) || 15000, '本地上传'); + }, uploadTimeoutMsForFile(file, context && context.timeoutMs), '本地上传'); var localPayload = await localResponse.json().catch(function() { return null; }); if (!localResponse.ok || !localPayload || !localPayload.asset) { throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败'); @@ -552,7 +561,7 @@ async function uploadFileToMediaAsset(file, plan, options, deps) { var localResult = await uploadLocalFolderAsset(file, plan, { rootUri: rootUri, documentId: documentId, - timeoutMs: 15000 + timeoutMs: uploadTimeoutMsForFile(file, 15000) }); var localAsset = localResult && localResult.asset ? localResult.asset : null; if (!localAsset) { @@ -753,6 +762,7 @@ window.__mnoteLocalUploadRuntime = { editorRootFromUploadOptions: editorRootFromUploadOptions, openEditorUploadFilePicker: openEditorUploadFilePicker, fetchWithTimeout: fetchWithTimeout, + uploadTimeoutMsForFile: uploadTimeoutMsForFile, uploadLocalFolderAsset: uploadLocalFolderAsset, uploadMediaAsset: uploadMediaAsset, insertUploadedAssetIntoEditor: insertUploadedAssetIntoEditor, diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js index 6e6c0745..694c475a 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-permission-runtime.js @@ -7,26 +7,133 @@ export function createSidebarPageAiPermissionRuntime(context) { } = context; const doc = documentRef || document; + function pageAiPermissionStorageKey() { + var sessionId = String(pageUiState.pageAiActiveSessionId || 'no-session').trim() || 'no-session'; + var path = doc && doc.location ? String(doc.location.pathname || '') : ''; + return 'mnote.page_ai.permission_queue.v1:' + path + ':' + sessionId; + } + + function pageAiPersistPermissionRequests() { + try { + var pending = pageUiState.pageAiPermissionRequests.filter(function(item) { + return item && item.kind === 'permission' && !item.resolved; + }).slice(-20); + if (window.localStorage) window.localStorage.setItem(pageAiPermissionStorageKey(), JSON.stringify(pending)); + } catch (_) {} + } + + function pageAiRestorePermissionRequests() { + try { + var raw = window.localStorage ? window.localStorage.getItem(pageAiPermissionStorageKey()) : ''; + var items = raw ? JSON.parse(raw) : []; + if (!Array.isArray(items)) items = []; + pageUiState.pageAiPermissionRequests = items.filter(function(item) { + return item && item.kind === 'permission' && !item.resolved; + }).slice(-20); + pageUiState.pageAiPermissionRequests.forEach(function(item) { + var exists = pageUiState.pageAiMessages.some(function(message) { + return message.kind === 'permission' && message.permissionId === item.permissionId; + }); + if (!exists) pageUiState.pageAiMessages.push(item); + }); + var pending = pageUiState.pageAiPermissionRequests.find(function(item) { return !item.resolved; }); + if (pending) { + if (!pageAiApplyPermissionMode(pending)) pageAiShowPermissionDialog(pending); + } + return pageUiState.pageAiPermissionRequests; + } catch (_) { + return []; + } + } + 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 detail = pageAiPermissionDetail(payload, permissionId); + var toolName = detail.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'; + var options = pageAiPermissionOptions(payload); return { role: 'tool', kind: 'permission', permissionId: permissionId, + runId: String(payload.runId || payload.run_id || pageUiState.pageAiCurrentRunId || '').trim(), toolName: toolName, - argsSummary: pageAiPreviewValue(args), + argsSummary: detail.summary || pageAiPreviewValue(args), content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'), resolved: decision === 'denied' || decision === 'allowed', - decision: decision + decision: decision, + options: options, + permissionDetails: detail }; } + function pageAiPermissionDetail(payload, permissionId) { + var params = payload && payload.params && typeof payload.params === 'object' ? payload.params : {}; + var toolCall = params.toolCall && typeof params.toolCall === 'object' ? params.toolCall : {}; + var rawInput = toolCall.rawInput && typeof toolCall.rawInput === 'object' ? toolCall.rawInput : {}; + var title = String(toolCall.title || payload.toolName || payload.tool || payload.name || '').trim(); + var kind = String(toolCall.kind || params.kind || '').trim(); + var command = String(rawInput.command || rawInput.cmd || '').trim(); + var path = String(rawInput.path || rawInput.file || rawInput.target || '').trim(); + var toolName = title || String(payload.toolName || payload.tool || payload.name || 'session/request_permission').trim(); + var primary = command || path || String(rawInput.pattern || rawInput.query || '').trim() || title || '权限请求'; + var rows = []; + rows.push({ label: '操作', value: toolName }); + if (kind) rows.push({ label: '类型', value: pageAiPermissionKindLabel(kind) }); + if (command) rows.push({ label: '命令', value: command }); + return { + toolName: toolName, + kind: kind, + kindLabel: pageAiPermissionKindLabel(kind), + command: command, + path: path, + summary: primary, + rows: rows + }; + } + + function pageAiPermissionKindLabel(kind) { + var normalized = String(kind || '').trim().toLowerCase(); + if (normalized === 'execute') return '执行命令'; + if (normalized === 'edit') return '写入文件'; + if (normalized === 'read') return '读取'; + if (normalized === 'other') return '其他'; + return kind || ''; + } + + function pageAiPermissionOptions(payload) { + var rawOptions = payload && Array.isArray(payload.options) + ? payload.options + : (payload && payload.params && Array.isArray(payload.params.options) ? payload.params.options : []); + return rawOptions.map(function(option) { + option = option && typeof option === 'object' ? option : {}; + var optionId = String(option.optionId || option.option_id || option.id || '').trim(); + if (!optionId) return null; + return { + optionId: optionId, + name: pageAiPermissionOptionLabel(optionId, option), + kind: String(option.kind || '').trim() + }; + }).filter(Boolean).slice(0, 8); + } + + function pageAiPermissionOptionLabel(optionId, option) { + var id = String(optionId || '').trim().toLowerCase(); + var raw = String((option && (option.name || option.title)) || '').trim(); + if (id === 'allow_once') return raw && !/^allow$/i.test(raw) ? raw.replace(/^Allow\b/i, '允许') : '允许一次'; + if (id === 'allow_always') return '本会话允许'; + if (id === 'allow_persistent') return '始终允许'; + if (id === 'reject_once' || id === 'reject') return '拒绝'; + if (id === 'cancel') return '取消'; + if (id === 'refine' || id === 'revise') return '要求修改'; + if (id === 'accept') return '接受'; + return raw || optionId || '选择'; + } + function pageAiApplyPermissionEvent(eventName, payloadText) { var payload = null; try { @@ -46,6 +153,10 @@ export function createSidebarPageAiPermissionRuntime(context) { pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) { return item.permissionId !== message.permissionId; }).concat([message]).slice(-20); + pageAiPersistPermissionRequests(); + if (!message.resolved && pageAiApplyPermissionMode(message)) { + return; + } if (!message.resolved) { pageAiShowPermissionDialog(message); } else { @@ -53,15 +164,76 @@ export function createSidebarPageAiPermissionRuntime(context) { } } - function pageAiResolvePermission(permissionId, decision) { + function pageAiReasonixApprovalMode() { + var preferences = pageUiState.pageAiSkillPreferences && typeof pageUiState.pageAiSkillPreferences === 'object' + ? pageUiState.pageAiSkillPreferences + : {}; + var mode = String(preferences['ai.agent.reasonix.approval_mode'] || 'ask').trim() || 'ask'; + return mode === 'allow' || mode === 'deny' ? mode : 'ask'; + } + + function pageAiApplyPermissionMode(message) { + if (!message || message.resolved) return false; + var mode = pageAiReasonixApprovalMode(); + if (mode === 'ask') return false; + var optionId = pageAiPermissionPreferredOptionId(message, mode); + window.setTimeout(function() { + pageAiResolvePermission(message.permissionId, mode, optionId); + }, 0); + return true; + } + + function pageAiPermissionPreferredOptionId(message, decision) { + var options = Array.isArray(message && message.options) ? message.options : []; + if (!options.length) return ''; + var allowKeywords = ['allow_once', 'allow', 'approve', 'yes', 'allow_always', 'allow_persistent']; + var denyKeywords = ['deny_once', 'reject_once', 'deny', 'reject', 'no', 'cancel', 'stop']; + var keywords = decision === 'allow' ? allowKeywords : denyKeywords; + for (var i = 0; i < keywords.length; i += 1) { + var keyword = keywords[i]; + var found = options.find(function(option) { + var text = [option && option.optionId, option && option.kind, option && option.name].map(function(value) { + return String(value || '').toLowerCase(); + }).join(' '); + return text.indexOf(keyword) >= 0; + }); + if (found && found.optionId) return String(found.optionId || ''); + } + if (decision === 'allow') { + var allowFallback = options.find(function(option) { return !pageAiPermissionOptionRejectLike(option); }); + return allowFallback && allowFallback.optionId ? String(allowFallback.optionId || '') : ''; + } + return ''; + } + + function pageAiResolvePermission(permissionId, decision, optionId) { permissionId = String(permissionId || '').trim(); + decision = String(decision || '').trim() || 'deny'; + optionId = String(optionId || '').trim(); if (!permissionId) return; - var runId = pageUiState.pageAiCurrentRunId; + var runId = String(pageUiState.pageAiCurrentRunId || '').trim(); + if (!runId && pageUiState.pageAiActiveSessionId && Array.isArray(pageUiState.pageAiSessions)) { + var activeSession = pageUiState.pageAiSessions.find(function(session) { + return session && session.id === pageUiState.pageAiActiveSessionId; + }); + runId = String(activeSession && (activeSession.runId || activeSession.hostRunId) || '').trim(); + } + if (!runId && Array.isArray(pageUiState.pageAiPermissionRequests)) { + var pending = pageUiState.pageAiPermissionRequests.find(function(item) { + return item && item.permissionId === permissionId; + }); + runId = String(pending && pending.runId || '').trim(); + } + if (!runId && doc && doc.documentElement) { + runId = String(doc.documentElement.getAttribute('data-mnote-page-ai-run-id') || doc.documentElement.getAttribute('data-mnote-page-ai-active-host-run-id') || '').trim(); + } if (runId) { + var body = { permissionId: permissionId, decision: decision }; + if (optionId) body.optionId = optionId; fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ permissionId: permissionId, decision: decision }) + body: JSON.stringify(body) }).then(function(response) { if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status); }).catch(function(err) { @@ -79,6 +251,7 @@ export function createSidebarPageAiPermissionRuntime(context) { }); var dialog = doc.querySelector('[data-page-ai-permission-dialog]'); if (dialog instanceof HTMLElement) dialog.hidden = true; + pageAiPersistPermissionRequests(); renderPageAiConversation(); } @@ -101,31 +274,70 @@ export function createSidebarPageAiPermissionRuntime(context) { dialog.innerHTML = '' + ''; doc.body.appendChild(dialog); } var tool = dialog.querySelector('[data-page-ai-permission-tool]'); - if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission'; + 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); - } - }); + if (args instanceof HTMLElement) args.innerHTML = pageAiPermissionDetailsHtml(message); + var actions = dialog.querySelector('[data-page-ai-permission-actions]'); + if (actions instanceof HTMLElement) actions.innerHTML = pageAiPermissionActionsHtml(message); dialog.hidden = false; } + function pageAiPermissionDetailsHtml(message) { + var details = message && message.permissionDetails && typeof message.permissionDetails === 'object' + ? message.permissionDetails + : {}; + var rows = Array.isArray(details.rows) ? details.rows : []; + if (!rows.length) { + return '
请求' + escapeHtml(message.argsSummary || message.content || '') + '
'; + } + return rows.slice(0, 6).map(function(row) { + return '
' + escapeHtml(row.label || '') + '' + escapeHtml(row.value || '') + '
'; + }).join(''); + } + + function pageAiPermissionActionsHtml(message) { + var options = Array.isArray(message.options) ? message.options : []; + if (!options.length) { + return '' + + '' + + ''; + } + return options.map(function(option) { + var denyLike = pageAiPermissionOptionRejectLike(option); + return ''; + }).join(''); + } + + function pageAiPermissionOptionRejectLike(option) { + var text = String((option && option.optionId) || '') + ' ' + String((option && option.kind) || '') + ' ' + String((option && option.name) || ''); + text = text.toLowerCase(); + return /reject|deny|cancel|stop|no/.test(text); + } + + function escapeAttr(value) { + return escapeHtml(String(value || '')); + } + + function escapeHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return { pageAiPermissionMessage, pageAiApplyPermissionEvent, pageAiResolvePermission, + pageAiRestorePermissionRequests, pageAiHidePermissionDialog, pageAiShowPermissionDialog }; diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-profile-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-profile-runtime.js index 56f6a2c8..b2b2562f 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-profile-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-profile-runtime.js @@ -163,6 +163,50 @@ export function createSidebarPageAiProfileRuntime(context) { }); } + function pageAiSessionStatusFilterValue(session) { + var status = String(session && session.status || '').trim(); + var mode = String(session && (session.runtimeMode || session.mode) || '').trim(); + if (pageUiState.pageAiActiveSessionId && session && session.id === pageUiState.pageAiActiveSessionId) return 'active'; + if (session && session.replaySeen) return 'replay_seen'; + if (mode === 'native_live' || mode === 'cold_resumed' || mode === 'replay_seen') return mode; + if (['running', 'tool_calling', 'queued', 'pending', 'acp_pending'].indexOf(status) >= 0) return 'active'; + if (['failed', 'aborted', 'cancelled', 'canceled'].indexOf(status) >= 0) return 'failed'; + if (status === 'completed') return 'completed'; + return status || 'unknown'; + } + + function pageAiSessionStatusLabel(value) { + return { + all: '全部状态', + active: 'Active', + completed: 'Completed', + failed: 'Failed', + native_live: 'Reasonix native-live', + cold_resumed: 'Cold resumed', + replay_seen: 'Replay seen', + unknown: 'Unknown' + }[value] || value; + } + + function pageAiSessionStatusFilterOptions(rows) { + var byValue = { + all: pageAiSessionStatusLabel('all'), + active: pageAiSessionStatusLabel('active'), + completed: pageAiSessionStatusLabel('completed'), + failed: pageAiSessionStatusLabel('failed'), + native_live: pageAiSessionStatusLabel('native_live'), + cold_resumed: pageAiSessionStatusLabel('cold_resumed'), + replay_seen: pageAiSessionStatusLabel('replay_seen') + }; + pageAiNormalizeArray(rows).filter(pageAiVisibleHistorySession).forEach(function(session) { + var value = pageAiSessionStatusFilterValue(session); + byValue[value] = pageAiSessionStatusLabel(value); + }); + return Object.keys(byValue).map(function(value) { + return { value: value, label: byValue[value] }; + }); + } + function pageAiVisibleHistorySession(session) { var source = String(session && session.source || '').trim(); var status = String(session && session.status || '').trim(); @@ -172,10 +216,11 @@ export function createSidebarPageAiProfileRuntime(context) { function pageAiFilteredHistoryRows(rows) { var filterValue = String(pageUiState.pageAiSessionAgentFilter || 'all').trim() || 'all'; + var statusFilter = String(pageUiState.pageAiSessionStatusFilter || 'all').trim() || 'all'; var normalized = pageAiNormalizeArray(rows).filter(pageAiVisibleHistorySession); - if (filterValue === 'all') return normalized; return normalized.filter(function(session) { - return pageAiSessionAgentFilterValue(session) === filterValue; + return (filterValue === 'all' || pageAiSessionAgentFilterValue(session) === filterValue) + && (statusFilter === 'all' || pageAiSessionStatusFilterValue(session) === statusFilter); }); } @@ -220,6 +265,9 @@ export function createSidebarPageAiProfileRuntime(context) { pageAiSessionAgentLabel, pageAiSessionPreviewText, pageAiSessionAgentFilterOptions, + pageAiSessionStatusFilterValue, + pageAiSessionStatusLabel, + pageAiSessionStatusFilterOptions, pageAiFilteredHistoryRows, pageAiTimestamp, pageAiUsageSummary diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js index 7ad80b76..72a8108e 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js @@ -39,6 +39,9 @@ export function createSidebarPageAiRenderRuntime(context) { pageAiSessionAgentFilterOptions, pageAiSessionAgentLabel, pageAiSessionAgentFilterValue, + pageAiSessionStatusFilterOptions, + pageAiSessionStatusFilterValue, + pageAiSessionStatusLabel, pageAiSessionPreviewText, pageAiSessionStorageLabel, pageAiSessionSelected, @@ -192,6 +195,50 @@ export function createSidebarPageAiRenderRuntime(context) { ''; } + function pageAiReasonixQuickControlLabel(key, fallback) { + if (key === 'ai.agent.reasonix.model_id') return '模型'; + if (key === 'ai.agent.reasonix.approval_mode') return '审批'; + if (key === 'ai.agent.reasonix.plan_mode') return 'Plan'; + return fallback || key; + } + + function pageAiRenderReasonixQuickControl(field) { + var key = String(field && field.key || '').trim(); + if (!key || field.secret === true) return ''; + var value = pageAiDescriptorFieldValue(field); + var options = pageAiDescriptorFieldOptions(field); + if (!options.length) return ''; + return '' + + ''; + } + + function pageAiReasonixQuickControlsHtml() { + if (pageAiCurrentAgentId() !== 'reasonix') return ''; + var descriptor = pageAiAgentDescriptor('reasonix') || pageAiAgentRecord('reasonix'); + var wanted = { + 'ai.agent.reasonix.model_id': true, + 'ai.agent.reasonix.approval_mode': true, + 'ai.agent.reasonix.plan_mode': true + }; + var fields = pageAiNormalizeArray(descriptor && descriptor.fields) + .filter(function(field) { return wanted[String(field && field.key || '').trim()] === true; }) + .map(pageAiRenderReasonixQuickControl) + .filter(Boolean) + .join(''); + if (!fields) return ''; + return '' + + '
' + + fields + + '
'; + } + function pageAiRenderAgentDescriptorCard(agentId, fallbackTitle) { var descriptor = pageAiAgentDescriptor(agentId) || pageAiAgentRecord(agentId); var title = String(descriptor && (descriptor.displayName || descriptor.label) || fallbackTitle || agentId).trim(); @@ -210,11 +257,27 @@ export function createSidebarPageAiRenderRuntime(context) { function pageAiCurrentModelLabel() { var profile = pageAiCurrentProfileRecord(); + var prefs = pageUiState.pageAiSkillPreferences || {}; var toolModel = pageAiMnoteToolModel(); + var reasonixModel = String(prefs['ai.agent.reasonix.model_id'] || '').trim(); + var approvalMode = String(prefs['ai.agent.reasonix.approval_mode'] || 'ask').trim() || 'ask'; + var planMode = String(prefs['ai.agent.reasonix.plan_mode'] || 'manual').trim() || 'manual'; + var approvalLabel = { + ask: '审批:询问', + allow: '审批:自动允许', + deny: '审批:自动拒绝' + }[approvalMode] || ('审批:' + approvalMode); + var planLabel = { + manual: '计划:手动', + auto: '计划:自动' + }[planMode] || ('计划:' + planMode); if (!profile) return 'tool: ' + toolModel; var model = String(profile.model || '').trim(); var gateway = String(profile.gateway || '').trim(); var profileLabel = [model, gateway].filter(Boolean).join(' · ') || '由 Hermes 决定'; + if (pageAiCurrentAgentId() === 'reasonix') { + return ['Reasonix', reasonixModel || toolModel, approvalLabel, planLabel].filter(Boolean).join(' · '); + } return 'tool: ' + toolModel + ' · profile: ' + profileLabel; } @@ -283,6 +346,88 @@ export function createSidebarPageAiRenderRuntime(context) { }); } + function pageAiRuntimeStatusValue(path, fallback) { + var node = pageUiState.pageAiRuntimeStatus || {}; + String(path || '').split('.').forEach(function(part) { + if (!part || node == null || typeof node !== 'object') return; + node = node[part]; + }); + if (node == null || node === '') return fallback || ''; + return String(node); + } + + function pageAiRuntimeStatusLabel() { + var status = pageUiState.pageAiRuntimeStatus || {}; + var runtime = String(status.runtime || pageUiState.pageAiAcpRuntime || 'reasonix'); + var mode = String(status.mode || 'new_session'); + if (runtime === 'reasonix') return pageAiCurrentModelLabel() + ' · ' + mode.replace(/_/g, '-'); + return runtime + ' · ' + mode.replace(/_/g, '-'); + } + + function pageAiRuntimeDetailHtml() { + var status = pageUiState.pageAiRuntimeStatus || {}; + var acp = status.acp && typeof status.acp === 'object' ? status.acp : {}; + var roots = status.roots && typeof status.roots === 'object' ? status.roots : {}; + var tools = status.tools && typeof status.tools === 'object' ? status.tools : {}; + var mcp = status.mcp && typeof status.mcp === 'object' ? status.mcp : {}; + var queue = status.session && status.session.queue && typeof status.session.queue === 'object' ? status.session.queue : {}; + var logs = status.logs && Array.isArray(status.logs.items) ? status.logs.items : []; + var rows = [ + ['runtime', pageAiRuntimeStatusLabel()], + ['session', String(status.mnoteSessionId || pageUiState.pageAiActiveSessionId || '无')], + ['acpSessionId', String(acp.acpSessionId || '无')], + ['load/replay', String(acp.loadStatus || 'not_applicable') + ' / replay=' + String(acp.replaySeen === true)], + ['live binding', acp.liveBinding && typeof acp.liveBinding === 'object' ? String(acp.liveBinding.status || 'unknown') : '无'], + ['roots', 'write=' + String(roots.write || 0) + ' readOnly=' + String(roots.readOnly || 0)], + ['MCP/tools', String(mcp.status || 'unknown') + ' · enabled=' + String(tools.enabled || 0) + ' disabled=' + String(tools.disabled || 0)], + ['queue', String(queue.queuedCount || pageUiState.pageAiQueueLength || 0)], + ['jobs', pageAiRuntimeStatusValue('jobs.running', '0')] + ]; + var html = rows.map(function(row) { + return '
' + escapeHtml(row[0]) + '' + escapeHtml(row[1]) + '
'; + }).join(''); + if (pageUiState.pageAiRuntimeStatusError) { + html += '
' + escapeHtml(pageUiState.pageAiRuntimeStatusError) + '
'; + } + if (logs.length) { + html += logs.slice(-4).map(function(item) { + return '
' + escapeHtml(String(item.kind || 'log')) + '' + escapeHtml(String(item.seq || '')) + '
'; + }).join(''); + } + return html || '
Runtime status 尚未加载。
'; + } + + function pageAiUsageDetailHtml() { + var status = pageUiState.pageAiRuntimeStatus || {}; + var usage = status.usage && typeof status.usage === 'object' ? status.usage : {}; + var rows = [ + ['last run', usage.lastRunTokens || usage.lastRunCost || 'unknown'], + ['session total', usage.sessionTokens || usage.sessionCost || 'unknown'], + ['cache', usage.cacheTokens || usage.cacheHit || 'unknown'] + ]; + return rows.map(function(row) { + return '
' + escapeHtml(row[0]) + '' + escapeHtml(String(row[1])) + '
'; + }).join(''); + } + + function pageAiSettingsTabsHtml(activeTab) { + return [ + ['agent', 'MNote'], + ['reasonix-settings', 'Reasonix'], + ['hermes-settings', 'Hermes'] + ].map(function(tab) { + var active = tab[0] === activeTab; + return ''; + }).join(''); + } + + function pageAiStatusTabsHtml() { + return '' + + '' + + '' + + ''; + } + function renderPageAiProviderButtons() { var drawer = ensurePageAiDrawer(); var isAcp = pageUiState.pageAiAcpRuntime !== ''; @@ -303,6 +448,8 @@ export function createSidebarPageAiRenderRuntime(context) { var isAcp = pageUiState.pageAiAcpRuntime !== ''; var activeAgentId = pageAiCurrentAgentId(); var activeProfile = pageAiCurrentProfile(); + if (pageUiState.pageAiPage === 'runtime') pageUiState.pageAiPage = 'status'; + if (pageUiState.pageAiPage === 'chat-only-settings') pageUiState.pageAiPage = 'agent'; drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat'); drawer.setAttribute('data-page-ai-active-agent-id', activeAgentId); drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix'); @@ -478,16 +625,32 @@ export function createSidebarPageAiRenderRuntime(context) { if (agentPanel instanceof HTMLElement) { agentPanel.innerHTML = '' + (pageUiState.pageAiAgentDescriptorError ? '
' + escapeHtml(pageUiState.pageAiAgentDescriptorError) + '
' : '') + + '
' + + '
MNote
上下文、权限、工具、会话历史和审计
' + + '
这里配置 MNote 发送给 agent 的 envelope,不配置 Reasonix 或 Hermes 原生 runtime。
' + + '
' + '
' + - '
授权区域
' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '
' + + '
授权区域
SQLite directory_grants
' + + '
' + '
' + - pageAiRenderAgentDescriptorCard(activeAgentId, pageAiCurrentAgentSelectionLabel()) + '
' + '
默认上下文
ContextRefs
' + + '
由 MNote 生成 run envelope,不写入 Reasonix/Hermes native memory。
' + '
' + '
' + - '
审计
changed files 默认摘要
' + + '
会话与审计
history / export / changed files
' + + '
删除会话只删除 MNote history;外部 Hermes/provider 会话不会默认删除。
' + '
'; + var mnoteAllowedRootsNode = agentPanel.querySelector('[data-page-ai-allowed-roots]'); + if (mnoteAllowedRootsNode instanceof HTMLElement) { + var mnoteAllowedRoots = pageAiBuildAllowedRoots(); + mnoteAllowedRootsNode.innerHTML = mnoteAllowedRoots.length + ? mnoteAllowedRoots.map(function(root) { + var label = root.rootUri.replace(/^file:\/\//, '') || root.rootUri; + return '' + escapeHtml(root.permission + ' · ' + label) + ''; + }).join('') + : '' + escapeHtml(String(pageUiState.pageAiAllowedRootsError || '').trim() || '未选择授权区域') + ''; + } } var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]'); if (profileSummary instanceof HTMLElement) profileSummary.textContent = '上下文可选'; @@ -512,6 +675,40 @@ export function createSidebarPageAiRenderRuntime(context) { var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : ''; runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix; } + drawer.querySelectorAll('[data-page-ai-runtime-badge]').forEach(function(runtimeBadge) { + if (runtimeBadge instanceof HTMLElement) runtimeBadge.textContent = pageAiRuntimeStatusLabel(); + }); + var runtimeActive = drawer.querySelector('[data-page-ai-runtime-active-run]'); + if (runtimeActive instanceof HTMLElement) { + var activeRunStatus = pageAiRuntimeStatusValue('activeRun.status', pageUiState.pageAiRunStatus || 'idle'); + runtimeActive.textContent = pageAiRunStatusLabel(activeRunStatus); + } + var runtimeAcp = drawer.querySelector('[data-page-ai-runtime-acp]'); + if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = pageAiRuntimeStatusValue('acp.acpSessionId', '无 ACP session'); + var runtimeDetail = drawer.querySelector('[data-page-ai-runtime-detail]'); + if (runtimeDetail instanceof HTMLElement) runtimeDetail.innerHTML = pageAiRuntimeDetailHtml(); + var jobsList = drawer.querySelector('[data-page-ai-jobs-list]'); + if (jobsList instanceof HTMLElement) { + var runtimeStatus = pageUiState.pageAiRuntimeStatus || {}; + var jobs = runtimeStatus.jobs && Array.isArray(runtimeStatus.jobs.items) ? runtimeStatus.jobs.items : []; + jobsList.innerHTML = jobs.length + ? jobs.map(function(job) { + return '
' + escapeHtml(String(job.id || job.name || 'job')) + '' + escapeHtml(String(job.status || 'running')) + '
'; + }).join('') + : '
暂无运行中的 job。
'; + } + var logsList = drawer.querySelector('[data-page-ai-logs-list]'); + if (logsList instanceof HTMLElement) { + var logStatus = pageUiState.pageAiRuntimeStatus || {}; + var logs = logStatus.logs && Array.isArray(logStatus.logs.items) ? logStatus.logs.items : []; + logsList.innerHTML = logs.length + ? logs.slice(-8).map(function(item) { + return '
' + escapeHtml(String(item.kind || 'log')) + '' + escapeHtml(String(item.seq || '')) + '
'; + }).join('') + : '
暂无 runtime logs。
'; + } + var usageDetail = drawer.querySelector('[data-page-ai-usage-detail]'); + if (usageDetail instanceof HTMLElement) usageDetail.innerHTML = pageAiUsageDetailHtml(); var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]'); if (stopButton instanceof HTMLButtonElement) { var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId; @@ -530,9 +727,10 @@ export function createSidebarPageAiRenderRuntime(context) { } else { queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) { var label = '队列 ' + (index + 1); + var preview = String(item.preview || item.traceId || item.queueId || '').trim(); return '' + '
' + - '
' + escapeHtml(label) + '
' + escapeHtml(item.queueId) + '
' + + '
' + escapeHtml(label) + '
' + escapeHtml(preview || item.queueId) + '
' + '' + '
'; }).join(''); @@ -547,7 +745,17 @@ export function createSidebarPageAiRenderRuntime(context) { : '等待 AI session'; } var modelNode = drawer.querySelector('[data-page-ai-model-status]'); - if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiBuildAllowedRoots().length ? '已授权' : '需授权'; + if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel(); + var reasonixControls = drawer.querySelector('[data-page-ai-reasonix-quick-controls]'); + if (reasonixControls instanceof HTMLElement) { + if (pageAiCurrentAgentId() === 'reasonix') { + reasonixControls.innerHTML = pageAiReasonixQuickControlsHtml().replace(/^]*>|<\/div>$/g, ''); + reasonixControls.hidden = false; + } else { + reasonixControls.innerHTML = ''; + reasonixControls.hidden = true; + } + } var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]'); if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope; var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]'); @@ -630,6 +838,20 @@ export function createSidebarPageAiRenderRuntime(context) { sessionAgentFilter.value = activeFilter; pageUiState.pageAiSessionAgentFilter = activeFilter; } + var sessionStatusFilter = drawer.querySelector('[data-page-ai-session-status-filter]'); + if (sessionStatusFilter instanceof HTMLSelectElement) { + var historyRowsForStatusFilter = pageUiState.pageAiSessionSearchResults.length + ? pageUiState.pageAiSessionSearchResults + : pageUiState.pageAiSessions; + var statusOptions = pageAiSessionStatusFilterOptions(historyRowsForStatusFilter); + var activeStatusFilter = String(pageUiState.pageAiSessionStatusFilter || 'all').trim() || 'all'; + if (!statusOptions.some(function(option) { return option.value === activeStatusFilter; })) activeStatusFilter = 'all'; + sessionStatusFilter.innerHTML = statusOptions.map(function(option) { + return ''; + }).join(''); + sessionStatusFilter.value = activeStatusFilter; + pageUiState.pageAiSessionStatusFilter = activeStatusFilter; + } var skillSearch = drawer.querySelector('[data-page-ai-skill-search]'); if (skillSearch instanceof HTMLInputElement && documentRef.activeElement !== skillSearch) { skillSearch.value = pageUiState.pageAiSkillQuery; @@ -754,7 +976,7 @@ export function createSidebarPageAiRenderRuntime(context) { } if (gatewayDetail instanceof HTMLElement) { if (!gatewayHealth) { - gatewayDetail.innerHTML = '
打开高级后会检查 agent runtime 与当前 profile。
'; + gatewayDetail.innerHTML = '
打开 Hermes 设置后会检查 gateway 与当前 profile。
'; } else { var gatewayInfo = gatewayHealth.gateway || {}; var profileInfo = gatewayHealth.profile || {}; @@ -798,8 +1020,8 @@ export function createSidebarPageAiRenderRuntime(context) { } var chatOnlyPanel = drawer.querySelector('[data-page-ai-chat-only-panel]'); if (chatOnlyPanel instanceof HTMLElement) { - chatOnlyPanel.innerHTML = pageAiRenderAgentDescriptorCard('chat_only', 'Chat-only') + - '
Chat-only
默认不申请文件写权限
'; + chatOnlyPanel.innerHTML = pageAiRenderAgentDescriptorCard('chat_only', 'API Chat / Chat-only profile') + + '
API Chat / Chat-only profile
默认不申请文件写权限,归属 MNote 设置。
'; } } @@ -859,6 +1081,11 @@ export function createSidebarPageAiRenderRuntime(context) { '' + '' + '' + + '' + '
' + '
' + '
' + @@ -885,26 +1112,36 @@ export function createSidebarPageAiRenderRuntime(context) { '
' + '' + '
' + - '' + - '' + - '' + - '' + - '' + + pageAiSettingsTabsHtml('agent') + '
' + '
' + '' + '' + - '
' + + '' + + '
' + + '
' + + '
' + + '
' + + 'MNote 工具' + + '暂无 tool call' + + '
' + + '' + + '
' + + '
' + '
' + '' + '' + - '' + '' + - '