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 发送给 agent 的 envelope,不配置 Reasonix 或 Hermes 原生 runtime。
' +
+ '' +
'' +
- '授权区域
' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '
' +
+ '授权区域
SQLite directory_grants
' +
+ '' +
'' +
- pageAiRenderAgentDescriptorCard(activeAgentId, pageAiCurrentAgentSelectionLabel()) +
'' +
'' +
+ '由 MNote 生成 run envelope,不写入 Reasonix/Hermes native memory。
' +
'' +
'' +
- '' +
+ '会话与审计
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') +
- '
';
+ 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' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
'' +
'
' +
'' +
'
' +
'
' +
- '' +
- '' +
- '' +
- '' +
- '' +
+ pageAiSettingsTabsHtml('hermes-settings') +
'
' +
'
' +
'' +
@@ -914,17 +1151,21 @@ export function createSidebarPageAiRenderRuntime(context) {
'' +
'
' +
'' +
+ '' +
+ '' +
+ '
' +
+ 'Hermes gateway' +
+ '未检查' +
+ '
' +
+ '
' +
+ '
' +
'' +
'' +
'
' +
'' +
'
' +
'
' +
- '' +
- '' +
- '' +
- '' +
- '' +
+ pageAiSettingsTabsHtml('reasonix-settings') +
'
' +
'
' +
'' +
@@ -935,26 +1176,13 @@ export function createSidebarPageAiRenderRuntime(context) {
'
' +
'' +
'' +
- '
' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
'
' +
'' +
'
' +
'
' +
- '' +
+ '' +
'' +
- '' +
+ '' +
'
' +
'
' +
'' +
@@ -974,47 +1202,51 @@ export function createSidebarPageAiRenderRuntime(context) {
'
' +
'
' +
'' +
- '
' +
+ '' +
'' +
'
' +
'
' +
- '' +
- '' +
- '' +
+ pageAiStatusTabsHtml() +
'
' +
'
' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- 'gateway' +
- '未检查' +
- '
' +
- '
' +
+ '
' +
+ '' +
+ '' +
'
' +
'
' +
'
' +
- 'queue' +
+ 'Current runtime' +
+ '未加载' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'Queue' +
'由 mnote-web 管理' +
'
' +
'
' +
'
' +
- '
' +
+ '
' +
'
' +
- 'MNote 工具调试' +
- '暂无 tool call' +
+ 'Jobs' +
+ '' +
'
' +
- '
' +
- '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'Logs tail' +
+ 'ACP stderr / tool failed / MCP failed' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ 'Usage' +
+ 'last run / session / cache' +
+ '
' +
+ '
' +
'
' +
'' +
'
' +
@@ -1034,6 +1266,10 @@ export function createSidebarPageAiRenderRuntime(context) {
'按 agent 筛选' +
'' +
'' +
+ '' +
'' +
'' +
'' +
@@ -1041,6 +1277,7 @@ export function createSidebarPageAiRenderRuntime(context) {
'