From dabaf03bd7687afe51568e94fa20874a775d6ea2 Mon Sep 17 00:00:00 2001 From: Agent Board Date: Thu, 25 Jun 2026 21:08:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...37-mnote-ui-foundation-design-system-v1.md | 438 ++++++ .../7-65-opencode-webui-embed-page-ai-v1.md | 555 +++++++ .../7-66-opencode-native-page-ai-ui-v1.md | 87 ++ .../process/7-67-openhub-page-ai-fusion-v1.md | 170 ++ ...68-openhub-weknora-mnote-deep-fusion-v1.md | 751 +++++++++ ...cle-page-ai-board-first-full-rewrite-v1.md | 439 ++++++ ...e-page-ai-board-first-productization-v1.md | 403 +++++ ...64-recycle-codexmobile-embed-page-ai-v1.md | 341 +++++ .../mnote-web/browser/mnote-ui-runtime.js | 69 + .../sidebar-page-ai-profile-runtime.js | 6 + .../browser/sidebar-page-ai-render-runtime.js | 156 +- .../browser/sidebar-page-ai-runtime.js | 1363 ++++++++++++++++- .../sidebar-page-ai-session-runtime.js | 37 +- .../browser/sidebar-page-settings-runtime.js | 12 +- .../mnote-web/src/hermes_tools/manifest.rs | 7 +- rust/crates/mnote-web/src/routes/mod.rs | 114 +- .../mnote-web/src/routes/page_ai_board.rs | 334 ++++ .../mnote-web/src/routes/page_ai_opencode.rs | 1124 ++++++++++++++ rust/crates/mnote-web/src/routes/ui_debug.rs | 7 +- rust/crates/mnote-web/src/routes/web_shell.rs | 14 + rust/crates/mnote-web/src/ssr/pages/layout.rs | 16 + rust/crates/mnote-web/src/ssr/styles.rs | 20 + rust/crates/mnote-web/src/ssr/styles/base.css | 5 - .../src/ssr/styles/components/page-ai.css | 295 ++++ .../src/ssr/styles/components/toast.css | 59 + .../mnote-web/src/ssr/styles/tokens.css | 9 + rust/spikes/leptos-tiptap-spike/Cargo.toml | 3 +- .../mnote-leptos-tiptap-spike-island.d.ts | 40 +- .../mnote-leptos-tiptap-spike-island.js | 74 +- .../mnote-leptos-tiptap-spike-island_bg.wasm | Bin 2095735 -> 2022544 bytes ...te-leptos-tiptap-spike-island_bg.wasm.d.ts | 40 +- .../src/editor_runtime/button.rs | 1 - .../src/editor_runtime/dismiss.rs | 16 +- .../src/editor_runtime/icons.rs | 3 +- scripts/TESTING_REFERENCE.md | 2 +- scripts/desktop-hot.js | 44 + scripts/desktop-hot.test.js | 1 + scripts/task490-runtime-surfaces-smoke.js | 27 +- scripts/task500-ui-debug-components-smoke.js | 13 +- scripts/task762-page-ai-board-first-smoke.js | 433 ++++++ .../task763-page-ai-opencode-embed-smoke.js | 298 ++++ ...k765-page-ai-opencode-host-static-smoke.js | 32 + 42 files changed, 7720 insertions(+), 138 deletions(-) create mode 100644 design/05-editor-mainline/process/5-37-mnote-ui-foundation-design-system-v1.md create mode 100644 design/07-ai/process/7-65-opencode-webui-embed-page-ai-v1.md create mode 100644 design/07-ai/process/7-66-opencode-native-page-ai-ui-v1.md create mode 100644 design/07-ai/process/7-67-openhub-page-ai-fusion-v1.md create mode 100644 design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-v1.md create mode 100644 design/old/07-ai/process/7-62-recycle-page-ai-board-first-full-rewrite-v1.md create mode 100644 design/old/07-ai/process/7-63-recycle-page-ai-board-first-productization-v1.md create mode 100644 design/old/07-ai/process/7-64-recycle-codexmobile-embed-page-ai-v1.md create mode 100644 rust/crates/mnote-web/browser/mnote-ui-runtime.js create mode 100644 rust/crates/mnote-web/src/routes/page_ai_board.rs create mode 100644 rust/crates/mnote-web/src/routes/page_ai_opencode.rs create mode 100644 rust/crates/mnote-web/src/ssr/styles/components/toast.css create mode 100644 scripts/task762-page-ai-board-first-smoke.js create mode 100644 scripts/task763-page-ai-opencode-embed-smoke.js create mode 100644 scripts/task765-page-ai-opencode-host-static-smoke.js diff --git a/design/05-editor-mainline/process/5-37-mnote-ui-foundation-design-system-v1.md b/design/05-editor-mainline/process/5-37-mnote-ui-foundation-design-system-v1.md new file mode 100644 index 00000000..b82be719 --- /dev/null +++ b/design/05-editor-mainline/process/5-37-mnote-ui-foundation-design-system-v1.md @@ -0,0 +1,438 @@ +# 5-37 [process] MNote UI Foundation — 统一设计系统与组件基础设施 v1 + +> 创建时间:2026-06-15 +> +> 状态:process +> +> Owner:05-editor-mainline / 03-rust-web / 11-wolai +> +> 目标:为 MNote 建立统一设计系统与组件基础设施,解决当前 UI 风格分散、组件缺失、交互 primitive 手写重复、CSS 单文件膨胀、SSR/CSR 两套运行时不统一的问题。 + +> 本轮完成 goal:`5-37-ui-foundation-p0-icon-qa`。范围是补齐 P0 中可独立落地的 token/portal/toast 基础设施,收紧 Material Symbols 图标验收,并交给 AgentBoard 做风险审查与浏览器 QA。P0 中编辑器全量 Button/Radix focus trap、CSS 全量拆分仍作为后续目标,不在本轮强行扩散。 + +## 1. 问题诊断 + +### 1.1 当前 UI 资产分布 + +| 层次 | 位置 | 规模 | 问题 | +|------|------|------|------| +| CSS | `styles.rs` | 6718 行单文件 Rust 字符串常量 | 单文件膨胀、无模块拆分、修改风险高 | +| SSR 模板 | `layout.rs`, `document.rs`, `home.rs` 等 | ~10 个 Leptos view 组件 | 交互逻辑混入 JS runtime,非 reactive | +| 浏览器 runtime | `browser/` | 46 个 JS 文件 | 覆盖全但 ad-hoc,无统一 primitive | +| 编辑器 CSR | `editor_runtime/` | 27 个 Rust 文件 | 菜单/overlay 手写,缺 radix 级基础设施 | + +### 1.2 核心缺口 + +**组件抽象层缺失** +- 没有 `Button/Dialog/Popover/Menu/Tabs/Input/Select/Toast/Skeleton` 等可复用 primitive。 +- root `package.json` 只有 Playwright + 预览库,无 UI 框架依赖。 +- recycle 旧前端有 Radix UI + shadcn 的完整组件体系(19 个组件文件),但依赖 React,不能直接用于当前 Rust SSR + CSR 架构。 + +**交互 primitive 重复手写** +- Escape 关闭:`tree-shell-filetree-menu-runtime.js`、`block_handle_menu_view.rs`、`mindmap_node_view.rs`、`sidebar-page-ai-runtime.js` 各写一份。 +- outside click 关闭:同上。 +- focus trap:仅 filetree menu 有基础实现,dialog 没有。 +- body scroll lock:不存在。 +- z-index 管理:无统一层叠规范,`1200`、`1000`、`999` 散落在 `styles.rs` 各处。 + +**反馈系统缺失** +- toast/notification 不存在。错误反馈多数用 `alert()` fallback 或局部 `set_command_feedback`。 +- 无全局反馈管道。 + +**图标系统不统一** +- 主壳用 Material Symbols (Google Fonts CDN)。 +- 编辑器菜单用 Unicode 文本符号(`"✦"` `"↻"` `"⌫"`)。 +- mindmap 用 emoji + Material Symbols 混合。 + +**设计 token 无管线** +- CSS 变量已在 `:root` 定义(`--wolai-bg`、`--wolai-text-primary` 等),但没有 token 文件、组件 variant 规范、尺寸等级、状态色。 +- 6718 行 CSS 直接塞在 Rust 常量里,长期维护不可持续。 + +**组件演示与可视化验收缺失** +- 无 Storybook 或内部 `/ui-debug` 组件画廊。 +- 现有 Wolai 视觉 smoke 偏页面级,不是组件级基线。 + +**前端工程守门缺失** +- root 没有 `lint`、`test`、`format` 脚本。 +- JS runtime 靠 smoke 和局部 `node --check`,无统一质量门禁。 + +**SSR / CSR 双运行时鸿沟** +- 主壳是 Leptos SSR → HTML 字符串 + 46 JS runtime。 +- 编辑器是 Leptos CSR/WASM → reactive signal。 +- 两个运行时之间没有共享的组件层、token 层或交互契约。 + +### 1.3 为什么不能直接搬旧 recycle 前端组件 + +- 旧前端依赖 React 19 + Radix UI React + shadcn + Tailwind + Next.js。 +- 当前主壳是 Rust Leptos SSR + 浏览器 JS runtime,编辑器是 Leptos CSR/WASM。 +- 不能把 React 组件直接嵌入 Rust 栈,需要适配。 + +## 2. 设计原则 + +1. **单一真相源**:token 只有一份定义,SSR 和 CSR 共同引用。 +2. **渐进迁移**:不在主壳大规模重写 SSR 架构;先在编辑器 island 落地 primitive,再逐步上溯到主壳。 +3. **headless 优先**:组件 primitive 不绑定视觉风格,外观由 Wolai 设计 token + CSS 变量控制。 +4. **事件驱动,取消轮询**:所有 UI 状态同步优先走 realtime event stream / MutationObserver / command result,不新增 `setInterval` 轮询。 +5. **可测试**:每个 primitive 必须有组件级 smoke,每个 variant/state 有截图或 DOM 断言覆盖。 +6. **不新增依赖债务**:只用成熟、有维护者的 Leptos 生态库;不自行从零实现已有成熟方案。 + +## 3. 技术选型 + +### 3.1 编辑器 island (CSR/WASM) 组件 primitive + +**选 radix-leptos v0.9.x** + +| 对比维度 | radix-leptos | thaw-ui | rust-ui/ui | leptonic | 自研 | +|---------|-------------|---------|------------|----------|------| +| 风格 | headless,不绑定视觉 | Fluent Design | shadcn/Tailwind | Material | 任意 | +| 与 Wolai CSS 兼容 | ✅ headless | ❌ Fluent 冲突 | ⚠️ 需 Tailwind | ❌ Material | ✅ | +| Leptos 0.8 兼容 | ✅ v0.9.0 | ✅ v0.5-beta | ✅ | ✅ | ✅ | +| 组件数量 | 57+ | ~40 | ~30 | ~20 | 0 | +| 测试覆盖 | 1792+ tests | 未知 | 未知 | 未知 | 0 | +| 维护状态 | 活跃 (Q2 2026) | 活跃 | 活跃 | 稳定 | N/A | +| WASM 体积 | 538KB 优化后 | 未知 | 未知 | 未知 | 0 | +| FocusTrap/Escape/Portal | ✅ 全套 | 未知 | 无 | 无 | 需自建 | + +结论:radix-leptos 是唯一提供完整 headless primitive + a11y infrastructure 的 Leptos 生态库,与 MNote Wolai CSS 变量体系零冲突。 + +### 3.2 主壳 (SSR + JS runtime) 过渡策略 + +短期不迁移 SSR 架构,但做三件事: + +1. **CSS 拆分**:`styles.rs` → `styles/tokens.css` + `styles/components/` + `styles/pages/`。 +2. **交互契约统一**:参考 radix-leptos 的 hook 行为语义,统一 JS runtime 中的 escape/focus trap/outside click 模式。 +3. **Portal 容器标准化**:`layout.rs` 中预留统一 portal root (`#mnote-portal-root`),所有 popover/dialog/menu 渲染到此处。 + +### 3.3 图标系统 + +统一使用 Material Symbols。 +- 编辑器菜单 Unicode 文本符号逐步替换为 Material Symbols。 +- 维护 `ICON_MAP` 常量将语义名映射到 ligature。 +- 不引入额外图标库。 + +### 3.4 构建工具 + +- **编辑器 island**:现有 `trunk` 构建不变,`Cargo.toml` 加 `radix-leptos-primitives` 依赖。 +- **主壳 CSS**:保持 Rust `include_str!` 编译期嵌入,但源文件拆分为多个 `.css` 文件。 +- **token 源**:一个 `tokens.css` 文件,被主壳和编辑器共同引用。 + +## 4. 设计 token 体系 + +### 4.1 设计变量分层 + +``` +Layer 0: Raw Colors (--color-basic-50 .. --color-basic-900) +Layer 1: Semantic Colors (--wolai-bg, --wolai-text-primary, --wolai-border, --wolai-brand ...) +Layer 2: Component Tokens (--mnote-btn-bg, --mnote-dialog-shadow, --mnote-popover-radius ...) +Layer 3: State Tokens (--mnote-state-hover, --mnote-state-active, --mnote-state-disabled) +``` + +Layer 0-1 已存在于 `styles.rs` 的 `:root` 块。Layer 2-3 当前缺失。 + +### 4.2 尺寸与间距等级 + +```css +--wolai-space-xs: 4px; +--wolai-space-sm: 8px; +--wolai-space-md: 12px; +--wolai-space-lg: 16px; +--wolai-space-xl: 24px; +--wolai-space-2xl: 32px; + +--wolai-radius-sm: 4px; +--wolai-radius-md: 6px; +--wolai-radius-lg: 8px; +--wolai-radius-xl: 12px; + +--wolai-font-xs: 12px; +--wolai-font-sm: 13px; +--wolai-font-md: 14px; +--wolai-font-lg: 16px; +--wolai-font-xl: 20px; +--wolai-font-2xl: 24px; +``` + +### 4.3 阴影等级 + +```css +--wolai-shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08); +--wolai-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.12); +--wolai-shadow-lg: 0 18px 54px rgba(15, 23, 42, 0.18); +--wolai-shadow-overlay: 0 18px 48px rgba(15, 23, 42, 0.22), 0 2px 8px rgba(15, 23, 42, 0.08); +``` + +### 4.4 z-index 层级 + +```css +--wolai-z-sidebar: 100; +--wolai-z-topbar: 200; +--wolai-z-floating: 500; +--wolai-z-popover: 1000; +--wolai-z-dialog-backdrop: 1100; +--wolai-z-dialog: 1200; +--wolai-z-toast: 1300; +--wolai-z-tooltip: 1400; +``` + +## 5. 组件清单与优先级 + +### 5.1 P0 — 编辑器 island 落地(blocking 风格统一) + +| 组件 | radix-leptos 对应 | 当前手写位置 | 迁移收益 | +|------|------------------|-------------|---------| +| Button | `Button` (6 variants) | 各处零散 `' + + '' + + '
' + workerSelect + modelSelect + workflowSelect + '
' + + '
MNote 只发送 capability/envelope;provider、执行和 workflow 由 Agent Board 负责。
'; + } else { var chatOnlyOptions = pageAiChatOnlyProfileEntries().map(function(profile) { var spec = pageAiChatOnlyProfileSpec(profile); var label = profile.menuLabel || (spec && spec.label) || pageAiProfileDisplayLabel(profile, ''); @@ -509,11 +568,12 @@ export function createSidebarPageAiRenderRuntime(context) { '' + '' + ''; + } } var agentChip = drawer.querySelector('[data-page-ai-agent-chip]'); if (agentChip instanceof HTMLElement) { - agentChip.textContent = activeAgentLabel; - agentChip.setAttribute('title', '当前 Agent:' + activeAgentLabel); + agentChip.textContent = boardFirst && typeof pageAiBoardWorkerLabel === 'function' ? pageAiBoardWorkerLabel() + ' / ' + (typeof pageAiBoardModelLabel === 'function' ? pageAiBoardModelLabel() : '默认') : activeAgentLabel; + agentChip.setAttribute('title', boardFirst ? '当前 worker/model:' + agentChip.textContent : '当前 Agent:' + activeAgentLabel); } var contextButton = drawer.querySelector('[data-page-ai-context-button]'); var contextPopoverId = 'mnote-page-ai-context-popover'; @@ -676,7 +736,10 @@ export function createSidebarPageAiRenderRuntime(context) { runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix; } drawer.querySelectorAll('[data-page-ai-runtime-badge]').forEach(function(runtimeBadge) { - if (runtimeBadge instanceof HTMLElement) runtimeBadge.textContent = pageAiRuntimeStatusLabel(); + if (runtimeBadge instanceof HTMLElement) { + var workflowLabel = boardFirst && typeof pageAiBoardWorkflowLabel === 'function' ? pageAiBoardWorkflowLabel() : ''; + runtimeBadge.textContent = boardFirst ? 'Agent Board · ' + workflowLabel : pageAiRuntimeStatusLabel(); + } }); var runtimeActive = drawer.querySelector('[data-page-ai-runtime-active-run]'); if (runtimeActive instanceof HTMLElement) { @@ -684,7 +747,7 @@ export function createSidebarPageAiRenderRuntime(context) { runtimeActive.textContent = pageAiRunStatusLabel(activeRunStatus); } var runtimeAcp = drawer.querySelector('[data-page-ai-runtime-acp]'); - if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = pageAiRuntimeStatusValue('acp.acpSessionId', '无 ACP session'); + if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = boardFirst ? 'provider 由 Board worker 决定' : 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]'); @@ -709,12 +772,12 @@ export function createSidebarPageAiRenderRuntime(context) { } 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; + var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId; + drawer.querySelectorAll('[data-page-ai-action="stop-run"]').forEach(function(stopButton) { + if (!(stopButton instanceof HTMLButtonElement)) return; stopButton.disabled = !canStop; stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true'); - } + }); var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]'); if (settingsLink instanceof HTMLButtonElement) { settingsLink.disabled = !pageAiHermesSettingsUrl(); @@ -771,6 +834,19 @@ export function createSidebarPageAiRenderRuntime(context) { panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage; } }); + if (boardFirst) { + var subtitle = drawer.querySelector('.wolai-page-ai-subtitle'); + if (subtitle instanceof HTMLElement) { + var workerLabel = typeof pageAiBoardWorkerLabel === 'function' ? pageAiBoardWorkerLabel() : 'ZCode 默认 developer'; + subtitle.innerHTML = 'Agent Board' + escapeHtml(workerLabel) + 'local-first 文件编辑'; + } + drawer.querySelectorAll('[data-page-ai-tab="agent"], [data-page-ai-tab="reasonix-settings"], [data-page-ai-tab="hermes-settings"]').forEach(function(node) { + if (node instanceof HTMLElement) node.hidden = true; + }); + drawer.querySelectorAll('[data-page-ai-panel="agent"], [data-page-ai-panel="reasonix-settings"], [data-page-ai-panel="hermes-settings"]').forEach(function(node) { + if (node instanceof HTMLElement) node.setAttribute('data-page-ai-legacy-provider-panel', 'true'); + }); + } var profileError = drawer.querySelector('[data-page-ai-profile-error]'); if (profileError instanceof HTMLElement) { profileError.textContent = pageUiState.pageAiProfileError || ''; @@ -1068,8 +1144,8 @@ export function createSidebarPageAiRenderRuntime(context) { '' + '' + '
' + - '' + - '' + + '' + + '' + '' + - '' + - '' + + '' + + '' + '
' + '' + '' + (item.retryPrompt ? '' : '') + '' + : ''; return '' + '
' + '
' + escapeHtml(roleLabel) + '
' + '
' + (item.role === 'assistant' ? renderPageAiMarkdown(item.content || '') : escapeHtml(item.content || '')) + '
' + + detailButton + '
'; } @@ -1669,6 +1756,38 @@ export function createSidebarPageAiRenderRuntime(context) { return titleKey === previewKey || previewKey.indexOf(titleKey) === 0 || titleKey.indexOf(previewKey) === 0; } + function pageAiBoardRunDetailHtml(runId) { + var current = typeof pageAiCurrentSession === 'function' ? pageAiCurrentSession() : null; + var detail = pageUiState.pageAiBoardRunDetails && pageUiState.pageAiBoardRunDetails[runId] + || current && current.boardRuns && current.boardRuns[runId] + || null; + if (!detail) return '
暂无 run detail。
'; + var changedFiles = pageAiNormalizeArray(detail.changedFiles).map(function(file) { + return '
  • ' + escapeHtml(String(file.path || file || '')) + (file.status ? ' · ' + escapeHtml(file.status) : '') + '
  • '; + }).join(''); + var verification = pageAiNormalizeArray(detail.verification).map(function(item) { + return '
  • ' + escapeHtml([item.command, item.status, item.output].filter(Boolean).join(' · ')) + '
  • '; + }).join(''); + var timeline = pageAiNormalizeArray(detail.timeline).map(function(event) { + return '
  • ' + escapeHtml([event.type, event.message, event.timestamp].filter(Boolean).join(' · ')) + '
  • '; + }).join(''); + return '' + + '
    ' + + '
    ' + + '
    Run detail' + escapeHtml(detail.runId || runId) + '
    ' + + '' + escapeHtml(detail.status || '') + '' + + '
    ' + + '
    ' + + '
    Worker' + escapeHtml(detail.workerName || detail.workerPresetId || '') + '
    ' + + '
    Model' + escapeHtml(detail.modelLabel || detail.modelOverride || '') + '
    ' + + '
    Workflow' + escapeHtml(detail.workflowName || detail.workflowId || '') + '
    ' + + '
    ' + + (changedFiles ? '
    changed files' + String(pageAiNormalizeArray(detail.changedFiles).length) + '
      ' + changedFiles + '
    ' : '') + + (verification ? '
    verification' + String(pageAiNormalizeArray(detail.verification).length) + '
      ' + verification + '
    ' : '') + + (timeline ? '
    timeline' + String(pageAiNormalizeArray(detail.timeline).length) + '
      ' + timeline + '
    ' : '') + + '
    '; + } + function renderPageAiConversation() { var drawer = ensurePageAiDrawer(); renderPageAiSuggestions(); @@ -1721,6 +1840,9 @@ export function createSidebarPageAiRenderRuntime(context) { var previousScrollTop = conversation.scrollTop; var openToolDetails = pageAiCaptureOpenToolDetails(conversation); conversation.innerHTML = pageAiConversationRenderHtml(pageUiState.pageAiMessages); + if (pageUiState.pageAiBoardActiveDetailRunId) { + conversation.insertAdjacentHTML('beforeend', pageAiBoardRunDetailHtml(pageUiState.pageAiBoardActiveDetailRunId)); + } pageAiRestoreOpenToolDetails(conversation, openToolDetails); if (shouldStickToBottom) { conversation.scrollTop = conversation.scrollHeight; diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js index ce59b365..99597ca9 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js @@ -21,7 +21,7 @@ export function createSidebarPageAiRuntime(context) { resolveWorkspaceId, searchText, } = context; - var PAGE_AI_SESSION_STORAGE_VERSION = 3; + var PAGE_AI_SESSION_STORAGE_VERSION = 4; // Page AI 请求合同:agentId 只负责路由;contextRefs 是用户勾选的上下文地址; // allowedRoots 只展示 SQLite directory_grants 解析结果,服务端仍会按当前用户重算; // runTargetSnapshot 必须来自 OpenEditorsSnapshot,避免运行中 UI 切换导致目标漂移。 @@ -55,6 +55,7 @@ export function createSidebarPageAiRuntime(context) { var PAGE_AI_DRAWER_DEFAULT_WIDTH = 440; var PAGE_AI_DRAWER_MIN_WIDTH = 340; var PAGE_AI_DRAWER_MAX_WIDTH = 760; + var PAGE_AI_OPENCODE_MODEL = { providerID: 'opencodego', modelID: 'deepseek-v4-flash' }; function clonePageAiDefaultValue(value) { if (Array.isArray(value)) return value.slice(); @@ -181,6 +182,23 @@ export function createSidebarPageAiRuntime(context) { pageAiActiveSkillSource: 'mnote', pageAiSessions: [], pageAiActiveSessionId: '', + pageAiBoardWorkerId: 'mnote-page-ai-zcode', + pageAiBoardWorkflowId: 'builtin-mnote-page-ai-chat', + pageAiBoardModelOverride: 'zcode-default', + pageAiBoardRunDetails: {}, + pageAiBoardActiveDetailRunId: '', + pageAiOpencodeStatus: null, + pageAiOpencodeStatusError: '', + pageAiOpencodeChangedFiles: [], + pageAiOpencodeDiffError: '', + pageAiOpencodeSessionId: '', + pageAiOpencodeBaseUrl: 'http://127.0.0.1:4096', + pageAiOpencodeIframeUrl: '', + pageAiOpencodeBindingKey: '', + pageAiOpencodeBindingInFlight: false, + pageAiOpencodeEventSource: null, + pageAiOpencodeEventSessionId: '', + pageAiOpencodeEventTimer: 0, pageAiSessionSearchQuery: '', pageAiSessionSearchResults: [], pageAiSessionSearchTimer: 0, @@ -663,6 +681,16 @@ export function createSidebarPageAiRuntime(context) { pageAiSkillSourceParts, pageAiToggleableSkillEntries, pageAiUsageSummary, + pageAiBoardFirstEnabled, + pageAiBoardWorkerOptions, + pageAiBoardWorkflowOptions, + pageAiBoardSelectedWorkerId, + pageAiBoardSelectedWorkflowId, + pageAiBoardWorkerLabel, + pageAiBoardWorkflowLabel, + pageAiBoardModelOptions, + pageAiBoardSelectedModelOverride, + pageAiBoardModelLabel, pageUiState, renderPageAiMarkdown, searchText, @@ -842,6 +870,1175 @@ export function createSidebarPageAiRuntime(context) { return '请按 Reasonix 自动计划模式处理:复杂或会写文件的任务先给出简短计划,并等待我批准后再执行。\n\n' + prompt; } + function pageAiBoardFirstEnabled() { + try { + return window.localStorage ? window.localStorage.getItem('mnote.page_ai.legacy_provider_mode') !== '1' : true; + } catch (_error) { + return true; + } + } + + function pageAiOpencodeHostEnabled() { + try { + return window.localStorage ? window.localStorage.getItem('mnote.page_ai.opencode_host') !== '0' : true; + } catch (_error) { + return true; + } + } + + function pageAiOpencodeStatusLabel(status) { + var node = status && typeof status === 'object' ? status : {}; + var state = String(node.status || node.state || node.runtimeStatus || '').trim(); + var ok = node.ok === true || node.ready === true || state === 'ready' || state === 'running' || state === 'ok'; + if (ok) return state ? '可用 · ' + state : '可用'; + if (state) return state; + return '未知'; + } + + function pageAiOpencodeSessionIdFromStatus(status) { + var node = status && typeof status === 'object' ? status : {}; + var session = node.session && typeof node.session === 'object' ? node.session : {}; + return String(node.sessionId || node.activeSessionId || node.currentSessionId || session.id || '').trim(); + } + + function pageAiOpencodeData(payload) { + return payload && payload.data && typeof payload.data === 'object' ? payload.data : payload; + } + + function pageAiOpencodeSessionArray(payload) { + var data = pageAiOpencodeData(payload); + return pageAiNormalizeArray(data && data.data) + .concat(pageAiNormalizeArray(data && data.sessions)) + .concat(Array.isArray(data) ? data : []); + } + + function pageAiOpencodeCollectChangedFileCandidates(value, output, depth) { + if (value == null || depth > 5) return; + if (typeof value === 'string') { + if (/\.(md|markdown|txt|json|ya?ml|toml|rs|js|ts|tsx|jsx|css|html)$/i.test(value) || value.indexOf('/') !== -1) { + output.push(value); + } + return; + } + if (Array.isArray(value)) { + value.forEach(function(item) { pageAiOpencodeCollectChangedFileCandidates(item, output, depth + 1); }); + return; + } + if (typeof value !== 'object') return; + var directPath = value.path || value.file || value.filename || value.relativePath || value.relative_path || value.absolutePath || value.absolute_path; + if (directPath) output.push(value); + ['changedFiles', 'changed_files', 'files', 'diff', 'diffs', 'changes', 'changed', 'edited', 'created', 'deleted', 'data', 'properties'].forEach(function(key) { + if (Object.prototype.hasOwnProperty.call(value, key)) pageAiOpencodeCollectChangedFileCandidates(value[key], output, depth + 1); + }); + } + + function pageAiOpencodeNormalizeChangedFiles(payload) { + var source = []; + pageAiOpencodeCollectChangedFileCandidates(payload, source, 0); + var seen = {}; + return source.map(function(item) { + var path = typeof item === 'string' + ? item + : String(item && (item.path || item.file || item.filename || item.relativePath || item.relative_path || item.absolutePath || item.absolute_path) || '').trim(); + var status = typeof item === 'object' && item ? String(item.status || item.change || item.kind || item.type || '').trim() : ''; + return { path: path, status: status }; + }).filter(function(item) { + if (!item.path || seen[item.path]) return false; + seen[item.path] = true; + return true; + }); + } + + function pageAiOpencodeCurrentPageLabel() { + var aggregate = currentPageAggregate(); + var meta = aggregate && aggregate.meta && typeof aggregate.meta === 'object' ? aggregate.meta : {}; + var target = currentPageAiEditorTarget() || {}; + return String(meta.title || target.title || document.title || currentDocumentId() || '当前页').trim(); + } + + function pageAiOpencodePrimaryPath() { + var target = currentPageAiEditorTarget() || {}; + var workspacePath = target && target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {}; + return String(workspacePath.relativePath || workspacePath.path || target.relativePath || localMarkdownRelativePathFromPageAiDocumentId(currentDocumentId()) || '').trim(); + } + + function pageAiOpencodeWritableLabel() { + var roots = pageAiNormalizeArray(pageUiState.pageAiAllowedRoots && pageUiState.pageAiAllowedRoots.length ? pageUiState.pageAiAllowedRoots : pageAiBuildAllowedRoots()); + if (!roots.length) return '授权待同步'; + var writable = roots.some(function(root) { + return /write|writable|rw/i.test(String(root && (root.permission || root.access || root.mode || '') || '')) || root && root.writable === true; + }); + return writable ? '可写' : '只读'; + } + + function pageAiFilePathFromRootUri(rootUri, relativePath) { + var root = String(rootUri || '').trim(); + var relative = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, ''); + if (!root || !relative || !root.startsWith('file://')) return ''; + try { + var pathname = new URL(root).pathname || ''; + return decodeURIComponent(pathname).replace(/\/+$/, '') + '/' + relative; + } catch (_error) { + return ''; + } + } + + function pageAiBuildOpencodeContextPayload() { + var selectedText = currentPageAiSelectedText(); + var target = currentPageAiEditorTarget() || {}; + var workspacePath = target && target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {}; + var pagePath = pageAiOpencodePrimaryPath(); + var rootUri = String(workspacePath.rootUri || target.rootUri || currentRootUri() || '').trim(); + return { + pageTitle: pageAiOpencodeCurrentPageLabel(), + pagePath: pagePath, + pageAbsolutePath: pageAiFilePathFromRootUri(rootUri, pagePath), + rootUri: rootUri, + selection: selectedText, + allowedRoots: pageAiNormalizeArray(pageUiState.pageAiAllowedRoots && pageUiState.pageAiAllowedRoots.length ? pageUiState.pageAiAllowedRoots : pageAiBuildAllowedRoots()), + pageId: String(currentDocumentId() || target.documentId || target.id || ''), + workspaceId: String(target.workspaceId || ''), + }; + } + + function pageAiOpencodeContextTextForInsert() { + var context = pageAiBuildOpencodeContextPayload(); + var lines = ['MNote 当前页上下文']; + if (context.pageTitle) lines.push('标题:' + context.pageTitle); + if (context.pagePath) lines.push('文件:' + context.pagePath); + if (context.selection) lines.push('选区:\n' + context.selection); + var roots = pageAiNormalizeArray(context.allowedRoots).map(function(root) { + return String(root && (root.path || root.root || root.uri || root.label) || '').trim(); + }).filter(Boolean).slice(0, 4); + if (roots.length) lines.push('允许目录:' + roots.join(', ')); + return lines.join('\n'); + } + + function pageAiPostOpencodeBridgeMessage(type, payload) { + var iframe = document.querySelector('[data-page-ai-opencode-iframe]'); + if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow) return false; + iframe.contentWindow.postMessage({ source: 'mnote-page-ai', target: 'opencode-webui', type: type, payload: payload || {} }, window.location.origin); + return true; + } + + function pageAiInstallOpencodeBridge() { + if (pageUiState.pageAiOpencodeBridgeInstalled) return; + pageUiState.pageAiOpencodeBridgeInstalled = true; + window.addEventListener('message', function(event) { + if (event.origin !== window.location.origin) return; + var message = event.data && typeof event.data === 'object' ? event.data : null; + if (!message || message.source !== 'mnote-opencode-bridge') return; + var payload = message.payload && typeof message.payload === 'object' ? message.payload : {}; + if (message.type === 'session-ready') { + pageUiState.pageAiOpencodeBridgeReady = true; + pageAiRenderOpencodeHostChrome(); + return; + } + if (message.type === 'open-file') { + pageAiOpenOpencodeChangedFile(payload.path || payload.href || payload.text || ''); + return; + } + if (message.type === 'refresh-file') { + pageAiRefreshOpencodeCurrentPage('page-ai-opencode-bridge'); + } + }); + } + + function pageAiOpencodeMessageInfo(message) { + return message && message.info && typeof message.info === 'object' ? message.info : message || {}; + } + + function pageAiOpencodePartText(part) { + if (!part || typeof part !== 'object') return ''; + if (part.text) return String(part.text); + if (part.content) return String(part.content); + if (part.summary) return String(part.summary); + if (part.type === 'patch') return '修改文件:' + pageAiNormalizeArray(part.files).join(', '); + if (part.type === 'file') return String(part.filename || part.url || 'file'); + if (part.type === 'tool') return String(part.tool || part.name || 'tool'); + return ''; + } + + function pageAiOpencodeIsContextEnvelope(message, parts) { + var info = pageAiOpencodeMessageInfo(message); + var text = String(info.text || '').trim() || pageAiNormalizeArray(parts).map(pageAiOpencodePartText).join('\n').trim(); + return text.indexOf('MNote context envelope') === 0; + } + + function pageAiOpencodeNormalizeMessages(payload) { + var data = pageAiOpencodeData(payload); + var items = pageAiNormalizeArray(data && data.data) + .concat(pageAiNormalizeArray(data && data.messages)) + .concat(Array.isArray(data) ? data : []); + return items.map(function(item) { + var info = pageAiOpencodeMessageInfo(item); + var parts = pageAiNormalizeArray(item && item.parts).concat(pageAiNormalizeArray(info.content)); + var type = String(info.role || info.type || item && item.type || '').toLowerCase(); + var role = type.indexOf('assistant') !== -1 ? 'assistant' : type.indexOf('user') !== -1 ? 'user' : type || 'event'; + return { + id: String(info.id || item && item.id || Math.random()).trim(), + role: role, + type: type, + agent: String(info.agent || ''), + model: info.model || null, + finish: String(info.finish || ''), + error: info.error || null, + cost: info.cost, + tokens: info.tokens || null, + parts: parts, + hiddenContext: pageAiOpencodeIsContextEnvelope(item, parts), + text: String(info.text || '').trim(), + }; + }).filter(function(message) { + return !message.hiddenContext && (message.text || message.parts.length || message.error || message.type); + }); + } + + function pageAiOpencodeNormalizePermissions(payload) { + var data = pageAiOpencodeData(payload); + return pageAiNormalizeArray(data && data.data) + .concat(pageAiNormalizeArray(data && data.permissions)) + .concat(Array.isArray(data) ? data : []); + } + + function pageAiOpencodeNormalizeTodo(payload) { + var data = pageAiOpencodeData(payload); + return pageAiNormalizeArray(data && data.data).concat(pageAiNormalizeArray(data && data.todo)).concat(Array.isArray(data) ? data : []); + } + + function pageAiOpencodeCompact(value, max) { + var text = typeof value === 'string' ? value : JSON.stringify(value || {}); + max = max || 260; + return text.length > max ? text.slice(0, max - 1) + '…' : text; + } + + function pageAiRenderOpencodePart(part) { + if (!part || typeof part !== 'object') return ''; + var type = String(part.type || 'part'); + if (type === 'text') return '
    ' + escapeHtml(pageAiOpencodePartText(part)).replace(/\n/g, '
    ') + '
    '; + if (type === 'reasoning') return '
    reasoning
    ' + escapeHtml(pageAiOpencodePartText(part)) + '
    '; + if (type === 'tool') { + var state = part.state && typeof part.state === 'object' ? part.state : {}; + var status = String(state.status || state.type || part.status || 'pending'); + return '
    ' + escapeHtml(part.tool || 'tool') + '' + escapeHtml(status) + '
    ' + escapeHtml(pageAiOpencodeCompact(state.output || state.input || state.error || state)) + '
    '; + } + if (type === 'patch') { + var files = pageAiNormalizeArray(part.files); + return '
    patch' + files.map(function(file) { + var path = String(file || '').trim(); + return ''; + }).join('') + '
    '; + } + if (type === 'file') { + var filePath = String(part.filename || part.url || '').trim(); + return '
    file
    '; + } + if (type === 'step-start' || type === 'step-finish' || type === 'snapshot' || type === 'agent' || type === 'subtask' || type === 'retry' || type === 'compaction') { + return '
    ' + escapeHtml(type) + '
    ' + escapeHtml(pageAiOpencodeCompact(part, 420)) + '
    '; + } + return '
    ' + escapeHtml(type) + '
    ' + escapeHtml(pageAiOpencodeCompact(part, 420)) + '
    '; + } + + function pageAiRenderOpencodeMessages(drawer) { + var node = drawer.querySelector('[data-page-ai-opencode-messages]'); + if (!(node instanceof HTMLElement)) return; + var messages = pageAiNormalizeArray(pageUiState.pageAiOpencodeMessages); + if (!messages.length) { + node.innerHTML = '
    输入消息后由 opencode 原生 runtime 回复;tool、reasoning、patch 会在这里显示。
    '; + return; + } + node.innerHTML = messages.map(function(message) { + var model = message.model && typeof message.model === 'object' ? String(message.model.providerID || message.model.provider || '') + '/' + String(message.model.id || message.model.modelID || '') : ''; + var parts = pageAiNormalizeArray(message.parts).map(pageAiRenderOpencodePart).join('') || (message.text ? '
    ' + escapeHtml(message.text).replace(/\n/g, '
    ') + '
    ' : ''); + var error = message.error ? '
    ' + escapeHtml(pageAiOpencodeCompact(message.error, 500)) + '
    ' : ''; + return '
    ' + + '
    ' + escapeHtml(message.role === 'assistant' ? 'opencode' : message.role === 'user' ? '你' : message.type || 'event') + '' + escapeHtml([message.agent, model, message.finish].filter(Boolean).join(' · ')) + '
    ' + + parts + error + + '
    '; + }).join(''); + node.scrollTop = node.scrollHeight; + } + + function pageAiRenderOpencodePermissions(drawer) { + var node = drawer.querySelector('[data-page-ai-opencode-permissions]'); + if (!(node instanceof HTMLElement)) return; + var permissions = pageAiNormalizeArray(pageUiState.pageAiOpencodePermissions); + node.innerHTML = permissions.map(function(item) { + var id = String(item && item.id || '').trim(); + var action = String(item && item.action || 'permission').trim(); + var resources = pageAiNormalizeArray(item && item.resources).join(', '); + return '
    ' + + '' + escapeHtml(action) + '' + + '' + escapeHtml(resources || '等待审批') + '' + + '' + + '' + + '' + + '
    '; + }).join(''); + } + + function pageAiRenderOpencodeSessions(drawer) { + var node = drawer.querySelector('[data-page-ai-opencode-sessions]'); + if (!(node instanceof HTMLElement)) return; + var sessions = pageAiNormalizeArray(pageUiState.pageAiOpencodeSessions).slice(0, 12); + node.innerHTML = sessions.length ? sessions.map(function(session) { + var id = String(session && session.id || '').trim(); + var active = id && id === String(pageUiState.pageAiOpencodeSessionId || ''); + var title = String(session && (session.title || session.name || session.id) || 'session').trim(); + var model = session && session.model && typeof session.model === 'object' ? String(session.model.providerID || '') + '/' + String(session.model.id || session.model.modelID || '') : ''; + return ''; + }).join('') : '暂无 session 列表'; + } + + function pageAiRenderOpencodeTodo(drawer) { + var node = drawer.querySelector('[data-page-ai-opencode-todo]'); + if (!(node instanceof HTMLElement)) return; + var todo = pageAiNormalizeArray(pageUiState.pageAiOpencodeTodo); + node.innerHTML = todo.length ? todo.map(function(item) { + return '
  • ' + escapeHtml(String(item && (item.content || item.text || item.title) || item || '')) + '
  • '; + }).join('') : ''; + } + + function pageAiRenderOpencodeHostChrome() { + var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); + if (!(drawer instanceof HTMLElement) || drawer.getAttribute('data-page-ai-opencode-host') !== 'true') return; + var status = pageUiState.pageAiOpencodeStatus || {}; + var session = status.session && typeof status.session === 'object' ? status.session : status; + var sessionId = String(pageUiState.pageAiOpencodeSessionId || pageAiOpencodeSessionIdFromStatus(status) || '').trim(); + var primaryPath = pageAiOpencodePrimaryPath(); + var titleNode = drawer.querySelector('[data-page-ai-opencode-current-page]'); + if (titleNode instanceof HTMLElement) titleNode.textContent = pageAiOpencodeCurrentPageLabel(); + var pathNode = drawer.querySelector('[data-page-ai-opencode-current-path]'); + if (pathNode instanceof HTMLElement) pathNode.textContent = primaryPath || '未定位真实 Markdown 路径'; + var selectionNode = drawer.querySelector('[data-page-ai-opencode-selection]'); + if (selectionNode instanceof HTMLElement) selectionNode.textContent = currentPageAiSelectedText() ? '已选中 ' + currentPageAiSelectedText().length + ' 字' : '无选区'; + var writableNode = drawer.querySelector('[data-page-ai-opencode-writable]'); + if (writableNode instanceof HTMLElement) writableNode.textContent = pageAiOpencodeWritableLabel(); + var statusNode = drawer.querySelector('[data-page-ai-opencode-runtime-status]'); + if (statusNode instanceof HTMLElement) { + statusNode.textContent = pageUiState.pageAiOpencodeStatusError || pageAiOpencodeStatusLabel(status); + statusNode.setAttribute('data-state', pageUiState.pageAiOpencodeStatusError ? 'error' : pageAiOpencodeStatusLabel(status)); + } + var sessionNode = drawer.querySelector('[data-page-ai-opencode-session]'); + if (sessionNode instanceof HTMLElement) sessionNode.textContent = sessionId ? 'session ' + sessionId : '未绑定 session'; + var modelNode = drawer.querySelector('[data-page-ai-opencode-model]'); + if (modelNode instanceof HTMLElement) { + var model = session.model && typeof session.model === 'object' ? String(session.model.providerID || '') + '/' + String(session.model.id || session.model.modelID || '') : ''; + modelNode.textContent = model || 'model 未知'; + } + var chipsNode = drawer.querySelector('[data-page-ai-opencode-changed-files]'); + if (chipsNode instanceof HTMLElement) { + var changedFiles = pageAiNormalizeArray(pageUiState.pageAiOpencodeChangedFiles); + chipsNode.innerHTML = changedFiles.length ? changedFiles.map(function(file) { + var path = String(file && file.path || '').trim(); + var label = path.split('/').filter(Boolean).pop() || path; + var statusText = String(file && file.status || '').trim(); + return ''; + }).join('') : '' + escapeHtml(pageUiState.pageAiOpencodeDiffError || '暂无 changed files') + ''; + } + var iframe = drawer.querySelector('[data-page-ai-opencode-iframe]'); + if (iframe instanceof HTMLIFrameElement) { + var nextUrl = String(pageUiState.pageAiOpencodeIframeUrl || '').trim(); + if (nextUrl && iframe.getAttribute('src') !== nextUrl) iframe.setAttribute('src', nextUrl); + iframe.hidden = !nextUrl; + } + var fallbackNode = drawer.querySelector('[data-page-ai-opencode-iframe-fallback]'); + if (fallbackNode instanceof HTMLElement) { + var hasUrl = Boolean(String(pageUiState.pageAiOpencodeIframeUrl || '').trim()); + fallbackNode.hidden = hasUrl && !pageUiState.pageAiOpencodeStatusError; + fallbackNode.textContent = pageUiState.pageAiOpencodeStatusError + ? 'opencode WebUI 暂不可用:' + pageUiState.pageAiOpencodeStatusError + : '正在连接 opencode WebUI…'; + } + } + + function pageAiEnsureOpencodeHostDrawer() { + var drawer = ensurePageAiDrawer(); + if (drawer.getAttribute('data-page-ai-opencode-host') !== 'true') { + drawer.setAttribute('data-page-ai-opencode-host', 'true'); + drawer.setAttribute('data-mnote-acp-runtime', 'opencode'); + drawer.innerHTML = '' + + '' + + ''; + } + pageAiInstallOpencodeBridge(); + pageAiRenderOpencodeHostChrome(); + void pageAiEnsureOpencodeSessionBinding(false); + return drawer; + } + + async function pageAiEnsureOpencodeSessionBinding(force) { + if (pageUiState.pageAiOpencodeBindingInFlight) return; + var binding = pageUiState.pageAiOpencodeStatus && typeof pageUiState.pageAiOpencodeStatus === 'object' ? pageUiState.pageAiOpencodeStatus : {}; + var context = pageAiBuildOpencodeContextPayload(); + var pathSignature = JSON.stringify([context.pageTitle, context.pagePath, context.pageAbsolutePath, context.rootUri, context.selection.length, JSON.stringify(context.allowedRoots).slice(0, 512)]); + if (!force && pageUiState.pageAiOpencodeSessionId && pageUiState.pageAiOpencodeBindingKey === pathSignature) return; + pageUiState.pageAiOpencodeBindingInFlight = true; + pageUiState.pageAiOpencodeBindingKey = pathSignature; + try { + var response = await fetch('/api/page-ai/opencode/session', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + cache: 'no-store', + body: JSON.stringify(Object.assign({ sessionId: force ? pageUiState.pageAiOpencodeSessionId || undefined : undefined, forceNew: force && !pageUiState.pageAiOpencodeSessionId }, context)), + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeStatus = payload.session || binding; + pageUiState.pageAiOpencodeStatusError = ''; + pageUiState.pageAiOpencodeSessionId = String(payload.sessionId || '').trim(); + pageUiState.pageAiOpencodeIframeUrl = String(payload.iframeUrl || pageUiState.pageAiOpencodeIframeUrl || '').trim(); + pageAiStartOpencodeEvents(); + void pageAiRefreshOpencodeProjection(); + pageAiRenderOpencodeHostChrome(); + } catch (error) { + pageUiState.pageAiOpencodeStatusError = error instanceof Error ? error.message : String(error); + pageUiState.pageAiOpencodeSending = false; + pageAiRenderOpencodeHostChrome(); + } finally { + pageUiState.pageAiOpencodeBindingInFlight = false; + } + } + + async function pageAiLoadOpencodeStatus() { + try { + var response = await fetch('/api/page-ai/opencode/status', { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeStatus = payload || {}; + pageUiState.pageAiOpencodeStatusError = ''; + pageUiState.pageAiOpencodeBaseUrl = String(payload && payload.baseUrl || pageUiState.pageAiOpencodeBaseUrl || 'http://127.0.0.1:4096').trim(); + pageUiState.pageAiOpencodeSessionId = pageAiOpencodeSessionIdFromStatus(payload) || pageUiState.pageAiOpencodeSessionId; + pageAiRenderOpencodeHostChrome(); + await pageAiRefreshOpencodeProjection(); + } catch (error) { + pageUiState.pageAiOpencodeStatusError = error instanceof Error ? error.message : String(error); + pageUiState.pageAiOpencodeSending = false; + pageAiRenderOpencodeHostChrome(); + } + } + + async function pageAiLoadOpencodeSessions() { + try { + var response = await fetch('/api/page-ai/opencode/sessions', { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeSessions = pageAiOpencodeSessionArray(payload && payload.sessions || payload); + pageAiRenderOpencodeHostChrome(); + } catch (error) { + pageUiState.pageAiOpencodeStatusError = error instanceof Error ? error.message : String(error); + pageUiState.pageAiOpencodeSending = false; + pageAiRenderOpencodeHostChrome(); + } + } + + async function pageAiLoadOpencodeMessages() { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) return; + var response = await fetch('/api/page-ai/opencode/messages?sessionId=' + encodeURIComponent(sessionId), { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeMessages = pageAiOpencodeNormalizeMessages(payload); + pageAiRenderOpencodeHostChrome(); + } + + async function pageAiLoadOpencodePermissions() { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) return; + var response = await fetch('/api/page-ai/opencode/permissions?sessionId=' + encodeURIComponent(sessionId), { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodePermissions = pageAiOpencodeNormalizePermissions(payload); + pageAiRenderOpencodeHostChrome(); + } + + async function pageAiLoadOpencodeTodo() { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) return; + var response = await fetch('/api/page-ai/opencode/todo?sessionId=' + encodeURIComponent(sessionId), { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (response.ok) pageUiState.pageAiOpencodeTodo = pageAiOpencodeNormalizeTodo(payload); + pageAiRenderOpencodeHostChrome(); + } + + async function pageAiLoadOpencodeDiff(sessionId) { + var id = String(sessionId || pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!id) return; + try { + var response = await fetch('/api/page-ai/opencode/diff?sessionId=' + encodeURIComponent(id), { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeChangedFiles = pageAiOpencodeNormalizeChangedFiles(payload); + pageUiState.pageAiOpencodeDiffError = ''; + pageAiRenderOpencodeHostChrome(); + if (pageAiOpencodeChangedFilesHitCurrentPage(pageUiState.pageAiOpencodeChangedFiles)) pageAiRefreshOpencodeCurrentPage('page-ai-opencode-event'); + } catch (error) { + pageUiState.pageAiOpencodeDiffError = error instanceof Error ? error.message : String(error); + pageAiRenderOpencodeHostChrome(); + } + } + + async function pageAiRefreshOpencodeProjection() { + if (!pageUiState.pageAiOpencodeSessionId) return; + await Promise.allSettled([ + pageAiLoadOpencodeMessages(), + pageAiLoadOpencodePermissions(), + pageAiLoadOpencodeTodo(), + pageAiLoadOpencodeDiff(pageUiState.pageAiOpencodeSessionId), + pageAiLoadOpencodeSessions(), + ]); + } + + function pageAiOpencodeChangedFilesHitCurrentPage(files) { + var currentPath = String(pageAiOpencodePrimaryPath() || '').trim(); + if (!currentPath) return false; + var normalizedCurrent = currentPath.replace(/\\/g, '/'); + return pageAiNormalizeArray(files).some(function(file) { + var path = String(file && file.path || '').trim().replace(/\\/g, '/'); + return path && (path === normalizedCurrent || path.endsWith('/' + normalizedCurrent.split('/').pop())); + }); + } + + function pageAiOpencodeEventMatchesSession(payload, sessionId) { + var id = String(sessionId || '').trim(); + if (!id) return false; + var node = payload && typeof payload === 'object' ? (payload.properties || payload.data || payload) : {}; + if (String(node.sessionID || node.sessionId || '').trim() === id) return true; + var text = ''; + try { text = JSON.stringify(payload || {}); } catch (_error) { text = ''; } + return text.indexOf(id) !== -1; + } + + function pageAiScheduleOpencodeRefresh(reason) { + if (!pageUiState.pageAiOpencodeSessionId) return; + if (pageUiState.pageAiOpencodeEventTimer) window.clearTimeout(pageUiState.pageAiOpencodeEventTimer); + pageUiState.pageAiOpencodeEventTimer = window.setTimeout(function() { + pageUiState.pageAiOpencodeEventTimer = 0; + void pageAiRefreshOpencodeProjection(); + }, reason === 'open' ? 250 : 700); + } + + function pageAiStartOpencodeEvents() { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId || !('EventSource' in window)) return; + var existing = pageUiState.pageAiOpencodeEventSource; + if (existing && pageUiState.pageAiOpencodeEventSessionId === sessionId) return; + if (existing && typeof existing.close === 'function') existing.close(); + var source = new EventSource('/api/page-ai/opencode/events'); + pageUiState.pageAiOpencodeEventSource = source; + pageUiState.pageAiOpencodeEventSessionId = sessionId; + var handleEvent = function(event) { + var payload = null; + try { payload = event && event.data ? JSON.parse(event.data) : null; } catch (_error) { payload = event && event.data ? { raw: String(event.data) } : null; } + var eventType = String(event && event.type || payload && (payload.type || payload.event) || 'message'); + var changedFiles = pageAiOpencodeNormalizeChangedFiles(payload); + if (changedFiles.length) { + pageUiState.pageAiOpencodeChangedFiles = changedFiles; + pageUiState.pageAiOpencodeDiffError = ''; + if (pageAiOpencodeChangedFilesHitCurrentPage(changedFiles)) pageAiRefreshOpencodeCurrentPage('page-ai-opencode-event'); + pageAiRenderOpencodeHostChrome(); + } + if (pageAiOpencodeEventMatchesSession(payload, sessionId) || /diff|file|permission|session|message/.test(eventType)) pageAiScheduleOpencodeRefresh('event'); + }; + source.onopen = function() { pageAiScheduleOpencodeRefresh('open'); }; + source.onmessage = handleEvent; + ['message.updated', 'message.part.updated', 'message.part.delta', 'session.diff', 'session.error', 'session.updated', 'session.next.text.delta', 'session.next.tool.called', 'session.next.tool.success', 'permission.v2.asked', 'permission.v2.replied', 'file.edited'].forEach(function(type) { + source.addEventListener(type, handleEvent); + }); + source.onerror = function() { + pageUiState.pageAiOpencodeDiffError = pageUiState.pageAiOpencodeDiffError || 'opencode event 暂不可用,保留手动刷新'; + pageAiRenderOpencodeHostChrome(); + }; + } + + async function pageAiSendOpencodeMessage(text) { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) await pageAiEnsureOpencodeSessionBinding(true); + sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) throw new Error('opencode session 未就绪'); + pageUiState.pageAiOpencodeMessages = pageAiNormalizeArray(pageUiState.pageAiOpencodeMessages); + pageUiState.pageAiOpencodeSending = true; + pageAiRenderOpencodeHostChrome(); + var response = await fetch('/api/page-ai/opencode/prompt', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + cache: 'no-store', + body: JSON.stringify({ sessionId: sessionId, text: text, model: PAGE_AI_OPENCODE_MODEL }) + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(payload && payload.error ? String(payload.error) : 'HTTP ' + response.status); + pageUiState.pageAiOpencodeSending = false; + pageAiScheduleOpencodeRefresh('prompt'); + } + + async function pageAiReplyOpencodePermission(id, response) { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId || !id) return; + await fetch('/api/page-ai/opencode/permission/reply', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + cache: 'no-store', + body: JSON.stringify({ sessionId: sessionId, requestId: id, response: response }) + }); + void pageAiLoadOpencodePermissions(); + } + + async function pageAiAbortOpencodeRun() { + var sessionId = String(pageUiState.pageAiOpencodeSessionId || '').trim(); + if (!sessionId) return; + await fetch('/api/page-ai/opencode/abort', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + cache: 'no-store', + body: JSON.stringify({ sessionId: sessionId }) + }); + pageAiScheduleOpencodeRefresh('abort'); + } + + function pageAiSelectOpencodeSession(sessionId) { + var id = String(sessionId || '').trim(); + if (!id) return; + pageUiState.pageAiOpencodeSessionId = id; + pageUiState.pageAiOpencodeMessages = []; + pageUiState.pageAiOpencodeLocalMessages = []; + pageAiStartOpencodeEvents(); + void pageAiRefreshOpencodeProjection(); + pageAiRenderOpencodeHostChrome(); + } + + function pageAiOpenOpencodeChangedFile(path) { + var targetPath = String(path || '').trim(); + if (!targetPath || !window.__mnoteDocumentPaneRuntime || typeof window.__mnoteDocumentPaneRuntime.openResourceInActiveTab !== 'function') return false; + void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ path: targetPath }); + return true; + } + + function pageAiRefreshOpencodeCurrentPage(reason) { + if (window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument === 'function') { + void window.__mnoteDocumentPaneRuntime.refreshPrimaryDocument({ reason: reason || 'page-ai-opencode-host' }); + return true; + } + return false; + } + + function pageAiApplyBoardFirstShell() { + if (!pageAiBoardFirstEnabled()) return; + var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); + if (!(drawer instanceof HTMLElement)) return; + if (drawer.getAttribute('data-page-ai-opencode-host') === 'true') return; + drawer.setAttribute('data-page-ai-board-first', 'true'); + var subtitle = drawer.querySelector('.wolai-page-ai-subtitle'); + if (subtitle instanceof HTMLElement) { + subtitle.innerHTML = 'Agent BoardZCode 默认 developerlocal-first 文件编辑'; + } + var runtimeStrip = drawer.querySelector('[data-page-ai-runtime-strip]'); + if (runtimeStrip instanceof HTMLElement) { + runtimeStrip.setAttribute('aria-label', '打开 Agent Board 运行状态'); + } + var runtimeBadge = drawer.querySelector('[data-page-ai-runtime-badge]'); + if (runtimeBadge instanceof HTMLElement) runtimeBadge.textContent = 'Agent Board · ' + pageAiBoardWorkflowLabel(); + var agentChip = drawer.querySelector('.wolai-page-ai-agent-chip'); + if (agentChip instanceof HTMLElement) { + agentChip.textContent = pageAiBoardWorkerLabel() + ' / ' + pageAiBoardModelLabel(); + agentChip.setAttribute('title', '当前 worker/model:' + pageAiBoardWorkerLabel() + ' / ' + pageAiBoardModelLabel()); + } + drawer.querySelectorAll('[data-page-ai-tab="agent"], [data-page-ai-tab="reasonix-settings"], [data-page-ai-tab="hermes-settings"]').forEach(function(node) { + if (node instanceof HTMLElement) node.hidden = true; + }); + drawer.querySelectorAll('[data-page-ai-panel="agent"], [data-page-ai-panel="reasonix-settings"], [data-page-ai-panel="hermes-settings"]').forEach(function(node) { + if (node instanceof HTMLElement) node.setAttribute('data-page-ai-legacy-provider-panel', 'true'); + }); + } + + function pageAiBoardDefaultWorkerId() { + return 'mnote-page-ai-zcode'; + } + + function pageAiBoardDefaultWorkflowId() { + return 'builtin-mnote-page-ai-chat'; + } + + function pageAiBoardDefaultModelOverride() { + return 'zcode-default'; + } + + function pageAiBoardDefaultModelOptions() { + return [ + { id: 'zcode-default', label: '默认', default: true }, + { id: 'zcode-fast', label: '快速' }, + { id: 'zcode-strong', label: '强力' } + ]; + } + + function pageAiBoardSelectedWorkerId() { + return String(pageUiState.pageAiBoardWorkerId || pageAiBoardDefaultWorkerId()).trim() || pageAiBoardDefaultWorkerId(); + } + + function pageAiBoardSelectedWorkflowId() { + return String(pageUiState.pageAiBoardWorkflowId || pageAiBoardDefaultWorkflowId()).trim() || pageAiBoardDefaultWorkflowId(); + } + + function pageAiBoardModelOptions(workerId) { + var selectedWorkerId = String(workerId || pageAiBoardSelectedWorkerId()).trim() || pageAiBoardDefaultWorkerId(); + var worker = pageAiBoardWorkerOptions().find(function(item) { return String(item.id || '') === selectedWorkerId; }) || {}; + var options = pageAiNormalizeArray(worker.modelOptions || worker.models || worker.allowedModels) + .map(function(option) { + if (typeof option === 'string') return { id: option, label: option }; + return { + id: String(option && (option.id || option.value || option.modelOverride || option.model) || '').trim(), + label: String(option && (option.label || option.name || option.id || option.value || option.modelOverride || option.model) || '').trim(), + default: Boolean(option && option.default) + }; + }) + .filter(function(option) { return option.id; }); + if (!options.length && selectedWorkerId === pageAiBoardDefaultWorkerId()) options = pageAiBoardDefaultModelOptions(); + return options; + } + + function pageAiBoardSelectedModelOverride() { + var options = pageAiBoardModelOptions(); + var fallback = (options.find(function(option) { return option.default; }) || options[0] || { id: pageAiBoardDefaultModelOverride() }).id; + var selected = String(pageUiState.pageAiBoardModelOverride || fallback || pageAiBoardDefaultModelOverride()).trim(); + if (!options.length) return selected || pageAiBoardDefaultModelOverride(); + return options.some(function(option) { return option.id === selected; }) ? selected : fallback; + } + + function pageAiBoardModelLabel(modelOverride) { + var id = String(modelOverride || pageAiBoardSelectedModelOverride()).trim(); + var option = pageAiBoardModelOptions().find(function(item) { return item.id === id; }); + return String(option && (option.label || option.id) || id || '默认').trim(); + } + + function pageAiBoardWorkerOptions() { + var byId = {}; + var fallback = { + id: pageAiBoardDefaultWorkerId(), + name: 'MNote 页面 AI · ZCode', + surface: 'mnote-page-ai', + role: 'developer', + agentType: 'zcode', + agentModel: 'default', + capabilities: ['text', 'repo-edit', 'terminal', 'mnote-capability-envelope', 'local-markdown-edit'], + modelOptions: pageAiBoardDefaultModelOptions() + }; + pageAiNormalizeArray(pageUiState.pageAiBoardWorkers).concat([fallback]).forEach(function(worker) { + var id = String(worker && (worker.id || worker.workerPresetId || worker.presetId) || '').trim(); + var surface = String(worker && (worker.surface || worker.productSurface) || '').trim(); + if (!id || id !== pageAiBoardDefaultWorkerId()) return; + if (surface && surface !== 'mnote-page-ai') return; + byId[id] = Object.assign({}, fallback, worker, { id: id, surface: 'mnote-page-ai' }); + }); + return Object.keys(byId).map(function(id) { return byId[id]; }); + } + + function pageAiBoardWorkflowOptions() { + var byId = {}; + var fallback = { id: pageAiBoardDefaultWorkflowId(), name: 'MNote 页面 AI', surface: 'mnote-page-ai' }; + pageAiNormalizeArray(pageUiState.pageAiBoardWorkflows).concat([fallback]).forEach(function(workflow) { + var id = String(workflow && (workflow.id || workflow.workflowId) || '').trim(); + var surface = String(workflow && (workflow.surface || workflow.productSurface) || '').trim(); + if (!id || id !== pageAiBoardDefaultWorkflowId()) return; + if (surface && surface !== 'mnote-page-ai') return; + byId[id] = Object.assign({}, fallback, workflow, { id: id, surface: 'mnote-page-ai' }); + }); + return Object.keys(byId).map(function(id) { return byId[id]; }); + } + + function pageAiBoardWorkerLabel(workerId) { + var id = String(workerId || pageAiBoardSelectedWorkerId()).trim(); + var worker = pageAiBoardWorkerOptions().find(function(item) { return String(item.id || '') === id; }); + return String(worker && (worker.name || worker.id) || id || 'Board worker').trim(); + } + + function pageAiBoardWorkflowLabel(workflowId) { + var id = String(workflowId || pageAiBoardSelectedWorkflowId()).trim(); + var workflow = pageAiBoardWorkflowOptions().find(function(item) { return String(item.id || '') === id; }); + return String(workflow && (workflow.name || workflow.id) || id || 'Board workflow').trim(); + } + + async function pageAiLoadBoardCatalog() { + if (!pageAiBoardFirstEnabled()) return; + try { + var workersResponse = await fetch('/api/page-ai/board/workers', { headers: { accept: 'application/json' }, cache: 'no-store' }); + var workersPayload = await workersResponse.json().catch(function() { return null; }); + if (workersResponse.ok) { + var workers = pageAiNormalizeArray(workersPayload && workersPayload.workers); + if (!workers.length) workers = pageAiNormalizeArray(workersPayload && workersPayload.workers && workersPayload.workers.workers); + pageUiState.pageAiBoardWorkers = workers; + } + var workflowsResponse = await fetch('/api/page-ai/board/workflows', { headers: { accept: 'application/json' }, cache: 'no-store' }); + var workflowsPayload = await workflowsResponse.json().catch(function() { return null; }); + if (workflowsResponse.ok) { + var workflows = pageAiNormalizeArray(workflowsPayload && workflowsPayload.workflows); + pageUiState.pageAiBoardWorkflows = workflows; + } + pageUiState.pageAiBoardCatalogError = ''; + } catch (error) { + pageUiState.pageAiBoardCatalogError = error instanceof Error ? error.message : String(error); + } + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + } + + function pageAiExtractFinalAnswer(summary) { + var text = String(summary || '').trim(); + if (!text) return ''; + var match = text.match(/(?:^|\n)##\s*FINAL_ANSWER\s*\n([\s\S]*?)(?=\n##\s+|$)/i); + if (match && match[1].trim()) return match[1].trim(); + return text + .replace(/^##\s*Completed\s*/i, '') + .replace(/\n##\s*Comments[\s\S]*$/i, '') + .replace(/\n##\s*Remaining[\s\S]*$/i, '') + .trim(); + } + + function pageAiFileUriToPath(rootUri) { + var value = String(rootUri || '').trim(); + if (!value.indexOf('file://') === 0 && value.slice(0, 7) !== 'file://') return ''; + try { + return decodeURIComponent(value.replace(/^file:\/\//, '')); + } catch (_error) { + return value.replace(/^file:\/\//, ''); + } + } + + function pageAiJoinPath(rootPath, relativePath) { + var root = String(rootPath || '').replace(/\/+$/, ''); + var rel = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, ''); + if (!root || !rel) return root || rel; + return root + '/' + rel; + } + + function pageAiBoardCapabilities() { + return [ + 'mnote.current_page.read', + 'mnote.selection.read', + 'mnote.open_resources.snapshot', + 'mnote.allowed_roots.describe', + 'mnote.lightrag.query', + 'mnote.reference.open', + 'mnote.page_aggregate.snapshot', + 'mnote.local_file.receipt' + ]; + } + + function pageAiBuildBoardEnvelope(prompt, scopedContext, runTargetSnapshot, contextRefs, allowedRoots, agentTargetPackage) { + var workspacePath = agentTargetPackage && agentTargetPackage.workspacePath && typeof agentTargetPackage.workspacePath === 'object' + ? agentTargetPackage.workspacePath + : (runTargetSnapshot && runTargetSnapshot.workspacePath && typeof runTargetSnapshot.workspacePath === 'object' ? runTargetSnapshot.workspacePath : {}); + var relativePath = String(workspacePath.relativePath || agentTargetPackage && agentTargetPackage.relativePath || runTargetSnapshot && runTargetSnapshot.relativePath || '').trim(); + var rootPath = pageAiFileUriToPath(currentRootUri()); + var absolutePath = String(workspacePath.absolutePath || workspacePath.path || '').trim() || pageAiJoinPath(rootPath, relativePath); + var selectedText = String(scopedContext && scopedContext.selectedText || '').trim(); + var writeIntent = /\b(edit|write|append|replace|fix|rewrite|update)\b|编辑|修改|追加|替换|修复|改写|更新/.test(String(prompt || '').toLowerCase()); + return { + schema: 'mnote.page_ai.board_task.v1', + intent: writeIntent ? 'edit' : 'ask', + message: prompt, + workspaceId: resolveWorkspaceId(document.body), + sourceKind: currentSourceKind(), + rootUri: currentRootUri(), + documentId: currentDocumentId(), + pageTitle: String((currentPageAggregate().meta || {}).title || document.title || '').trim(), + board: { + workflowId: pageAiBoardSelectedWorkflowId(), + workflowName: pageAiBoardWorkflowLabel(), + workerPresetId: pageAiBoardSelectedWorkerId(), + workerName: pageAiBoardWorkerLabel(), + modelOverride: pageAiBoardSelectedModelOverride(), + modelLabel: pageAiBoardModelLabel() + }, + primaryTarget: { + kind: 'markdown_file', + relativePath: relativePath, + absolutePath: absolutePath, + documentId: currentDocumentId(), + workspacePath: workspacePath + }, + selection: { + text: selectedText, + range: null + }, + contextRefs: contextRefs, + allowedRoots: allowedRoots, + capabilities: pageAiBoardCapabilities(), + modelOverride: pageAiBoardSelectedModelOverride(), + pageContext: pageAiPageContextForRefs(scopedContext.pageContext, contextRefs), + runTargetSnapshot: runTargetSnapshot, + targetPackage: agentTargetPackage, + writePolicy: { + allowFileWrite: allowedRoots.some(function(root) { return String(root.permission || '').indexOf('write') >= 0; }), + requireHumanApproval: false, + forbidMnoteInternalStateWrite: true + } + }; + } + + function pageAiBoardReceiptSummary(payload) { + var receipt = payload && payload.receipt && typeof payload.receipt === 'object' ? payload.receipt : null; + var board = payload && payload.board && typeof payload.board === 'object' ? payload.board : null; + if (receipt && String(receipt.summary || '').trim()) return String(receipt.summary || '').trim(); + var nodeRuns = Array.isArray(board && board.nodeRuns) ? board.nodeRuns : []; + for (var i = nodeRuns.length - 1; i >= 0; i -= 1) { + if (String(nodeRuns[i] && nodeRuns[i].outputSummary || '').trim()) return String(nodeRuns[i].outputSummary).trim(); + } + var events = Array.isArray(board && board.events) ? board.events : []; + if (events.length) return String(events[events.length - 1].message || '').trim(); + return ''; + } + + function pageAiBoardRunStatus(payload) { + var board = payload && payload.board && typeof payload.board === 'object' ? payload.board : null; + var run = board && board.run && typeof board.run === 'object' ? board.run : (board && board.id ? board : null); + return String(run && run.status || '').trim(); + } + + function pageAiBoardRunId(payload) { + var board = payload && payload.board && typeof payload.board === 'object' ? payload.board : null; + var run = board && board.run && typeof board.run === 'object' ? board.run : (board && board.id ? board : null); + return String(run && run.id || payload && payload.runId || payload && payload.id || '').trim(); + } + + function pageAiBoardReceiptObject(payload) { + var receipt = payload && payload.receipt && typeof payload.receipt === 'object' ? payload.receipt : null; + if (receipt && receipt.receipt && typeof receipt.receipt === 'object') return receipt.receipt; + return receipt; + } + + function pageAiBoardFinalAnswer(payload) { + var receipt = pageAiBoardReceiptObject(payload); + var candidates = [ + receipt && receipt.finalAnswer, + receipt && receipt.summary, + receipt && receipt.outputSummary, + pageAiBoardReceiptSummary(payload) + ]; + for (var i = 0; i < candidates.length; i += 1) { + var text = pageAiExtractFinalAnswer(candidates[i]); + if (text) return text; + } + return ''; + } + + function pageAiBoardChangedFiles(payload) { + var receipt = pageAiBoardReceiptObject(payload) || {}; + var changed = pageAiNormalizeArray(receipt.changedFiles || receipt.changed_files || receipt.filesChanged); + return changed.map(function(file) { + if (typeof file === 'string') return { path: file }; + return { + path: String(file && (file.path || file.absolutePath || file.relativePath || file.file) || '').trim(), + status: String(file && (file.status || file.changeType || file.kind) || '').trim() + }; + }).filter(function(file) { return file.path; }); + } + + function pageAiBoardTimeline(payload) { + var board = payload && payload.board && typeof payload.board === 'object' ? payload.board : null; + var events = pageAiNormalizeArray(board && board.events); + if (!events.length && board && board.run && Array.isArray(board.run.events)) events = board.run.events; + return pageAiNormalizeArray(events).map(function(event) { + var timestamp = String(event && (event.timestamp || event.createdAt || event.created_at || event.time) || '').trim(); + var type = String(event && (event.type || event.eventType || event.event || event.status) || '').trim(); + var message = String(event && (event.message || event.summary || event.title || '') || '').trim(); + return { timestamp: timestamp, type: type, message: message }; + }).filter(function(event) { return event.type || event.message; }); + } + + function pageAiBoardVerification(payload) { + var receipt = pageAiBoardReceiptObject(payload) || {}; + return pageAiNormalizeArray(receipt.verification || receipt.checks).map(function(item) { + if (typeof item === 'string') return { command: item, status: '' }; + return { + command: String(item && (item.command || item.name || item.label) || '').trim(), + status: String(item && (item.status || item.result) || '').trim(), + output: String(item && (item.output || item.summary) || '').trim() + }; + }).filter(function(item) { return item.command || item.status || item.output; }); + } + + function pageAiBuildBoardRunDetail(runId, payload) { + var status = pageAiBoardRunStatus(payload); + return { + schema: 'mnote.page_ai.board_run_detail.v1', + runId: runId, + status: status === 'complete' ? 'completed' : (status || 'completed'), + workflowId: pageAiBoardSelectedWorkflowId(), + workflowName: pageAiBoardWorkflowLabel(), + workerPresetId: pageAiBoardSelectedWorkerId(), + workerName: pageAiBoardWorkerLabel(), + modelOverride: pageAiBoardSelectedModelOverride(), + modelLabel: pageAiBoardModelLabel(), + finalAnswer: pageAiBoardFinalAnswer(payload), + changedFiles: pageAiBoardChangedFiles(payload), + verification: pageAiBoardVerification(payload), + timeline: pageAiBoardTimeline(payload), + receipt: pageAiBoardReceiptObject(payload) || null, + payload: payload || null, + updatedAt: Date.now() + }; + } + + function pageAiStoreBoardRunDetail(runId, payload) { + if (!runId) return null; + if (!pageUiState.pageAiBoardRunDetails || typeof pageUiState.pageAiBoardRunDetails !== 'object') { + pageUiState.pageAiBoardRunDetails = {}; + } + var detail = pageAiBuildBoardRunDetail(runId, payload); + pageUiState.pageAiBoardRunDetails[runId] = detail; + var current = pageAiCurrentSession(); + if (current) { + current.boardRuns = Object.assign({}, current.boardRuns || {}, { [runId]: detail }); + current.boardRunId = runId; + current.workflowId = detail.workflowId; + current.workerPresetId = detail.workerPresetId; + current.modelOverride = detail.modelOverride; + current.receiptId = String(detail.receipt && (detail.receipt.id || detail.receipt.receiptId) || runId || '').trim(); + } + return detail; + } + + async function pageAiPollBoardRun(runId) { + var lastPayload = null; + for (var attempt = 0; attempt < 360; attempt += 1) { + var response = await fetch('/api/page-ai/board/runs/' + encodeURIComponent(runId), { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'board_run_status_failed_' + response.status)); + lastPayload = payload; + var status = pageAiBoardRunStatus(payload); + if (status) pageAiSetRunStatus(status === 'complete' ? 'completed' : status); + renderPageAiControls(); + if (['complete', 'failed', 'blocked', 'cancelled'].indexOf(status) >= 0) return payload; + await new Promise(function(resolve) { window.setTimeout(resolve, attempt < 8 ? 1000 : 2500); }); + } + return lastPayload; + } + + async function sendPageAiBoardMessage(text) { + if (pageUiState.pageAiBusy) return; + var prompt = searchText(text); + if (!prompt) return; + pageUiState.pageAiBusy = true; + pageAiSetRunStatus('preparing'); + var currentSession = pageAiCurrentSession(); + try { + var contextSnapshot = currentPageAiContextSnapshot(); + var scopedContext = pageAiScopedPageContext(contextSnapshot); + scopedContext.editorTarget = currentPageAiScopedEditorTarget(); + assertPageAiTargetInCurrentWorkspace(scopedContext.editorTarget); + if (currentSourceKind() === 'local_folder' && !pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).length) { + await pageAiLoadAllowedRoots(); + } + await assertPageAiTargetWritable(scopedContext.editorTarget); + var runTargetSnapshot = pageAiBuildRunTargetSnapshot(scopedContext, prompt); + var contextRefs = pageAiBuildContextRefs(scopedContext, runTargetSnapshot); + var allowedRoots = pageAiBuildAllowedRoots(); + var agentTargetPackage = pageAiBuildAgentTargetPackage(scopedContext, runTargetSnapshot); + assertPageAiLocalWritePermission(prompt, agentTargetPackage); + var envelope = pageAiBuildBoardEnvelope(prompt, scopedContext, runTargetSnapshot, contextRefs, allowedRoots, agentTargetPackage); + pageUiState.pageAiMessages.push({ role: 'user', content: prompt }); + pageUiState.pageAiMessages.push({ role: 'assistant', kind: 'status', content: '正在处理…', streaming: true }); + if (currentSession) { + if (currentSession.title === '新会话') currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt; + currentSession.schema = 'mnote.page_ai.session.v2'; + currentSession.source = 'board'; + currentSession.workerPresetId = pageAiBoardSelectedWorkerId(); + currentSession.workflowId = pageAiBoardSelectedWorkflowId(); + currentSession.modelOverride = pageAiBoardSelectedModelOverride(); + currentSession.messages = pageUiState.pageAiMessages.slice(); + currentSession.updatedAt = Date.now(); + } + pageAiPersistSessions(); + renderPageAiConversation(); + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + var response = await fetch('/api/page-ai/board/runs', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify({ + workflowId: pageAiBoardSelectedWorkflowId(), + workerPresetId: pageAiBoardSelectedWorkerId(), + modelOverride: pageAiBoardSelectedModelOverride(), + message: prompt, + envelope: envelope, + autoRun: true, + priority: 'high' + }) + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'board_run_failed_' + response.status)); + var runId = pageAiBoardRunId(payload); + pageUiState.pageAiCurrentRunId = runId; + if (runId) { + pageUiState.pageAiMessages = pageUiState.pageAiMessages.filter(function(message) { return !(message && message.kind === 'status' && message.streaming); }); + pageUiState.pageAiMessages.push({ role: 'assistant', kind: 'status', content: 'Agent Board 正在处理…', streaming: true, boardRunId: runId }); + } + pageAiSetRunStatus('running'); + renderPageAiConversation(); + pageAiApplyBoardFirstShell(); + var finalPayload = runId ? await pageAiPollBoardRun(runId) : payload; + var detail = pageAiStoreBoardRunDetail(runId, finalPayload); + var finalAnswer = detail && detail.finalAnswer || 'Agent Board run 已结束,但没有返回 final answer。'; + pageUiState.pageAiMessages = pageUiState.pageAiMessages.filter(function(message) { return !(message && message.kind === 'status' && message.streaming); }); + pageUiState.pageAiMessages.push({ + role: 'assistant', + content: finalAnswer, + boardRunId: runId, + workflowId: pageAiBoardSelectedWorkflowId(), + workerPresetId: pageAiBoardSelectedWorkerId(), + modelOverride: pageAiBoardSelectedModelOverride(), + retryPrompt: prompt, + receiptId: String(detail && detail.receipt && (detail.receipt.id || detail.receipt.receiptId) || runId || '').trim() + }); + if (currentSession) { + currentSession.schema = 'mnote.page_ai.session.v2'; + currentSession.source = 'board'; + currentSession.messages = pageUiState.pageAiMessages.slice(); + currentSession.updatedAt = Date.now(); + currentSession.boardRunId = runId; + currentSession.workflowId = pageAiBoardSelectedWorkflowId(); + currentSession.workerPresetId = pageAiBoardSelectedWorkerId(); + currentSession.modelOverride = pageAiBoardSelectedModelOverride(); + } + pageAiPersistSessions(); + pageAiSetRunStatus(pageAiBoardRunStatus(finalPayload) === 'complete' ? 'completed' : pageAiBoardRunStatus(finalPayload) || 'completed'); + renderPageAiConversation(); + pageAiApplyBoardFirstShell(); + } catch (error) { + pageAiSetRunStatus('failed'); + pageUiState.pageAiMessages.push({ role: 'assistant', content: 'Agent Board 处理失败:' + (error instanceof Error ? error.message : String(error)) }); + renderPageAiConversation(); + pageAiApplyBoardFirstShell(); + } finally { + pageUiState.pageAiBusy = false; + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + } + } + function pageAiApplyAgentDescriptorsPayload(payload) { var descriptors = pageAiNormalizeArray(payload && payload.descriptors) .map(pageAiNormalizeAgentDescriptor) @@ -2007,6 +3204,17 @@ export function createSidebarPageAiRuntime(context) { if (!runId || pageUiState.pageAiRunStatus === 'idle' || pageUiState.pageAiRunStatus === 'completed') return; pageUiState.pageAiStoppedRunIds[runId] = true; try { + if (pageAiBoardFirstEnabled()) { + var boardResponse = await fetch('/api/page-ai/board/runs/' + encodeURIComponent(runId) + '/cancel', { + method: 'POST', + headers: { accept: 'application/json' } + }); + var boardPayload = await boardResponse.json().catch(function(){ return null; }); + if (!boardResponse.ok) throw new Error(pageAiErrorMessage(boardPayload, 'board_run_cancel_failed_' + boardResponse.status)); + pageAiSetRunStatus('cancelled', runId); + pageUiState.pageAiMessages.push({ role: 'assistant', content: '已请求停止当前 Board run。', boardRunId: runId }); + return; + } var response = await fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/abort', { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -2332,6 +3540,21 @@ export function createSidebarPageAiRuntime(context) { function openPageAiDrawer() { + if (pageAiOpencodeHostEnabled()) { + pageAiEnsureContextRefState(); + void pageAiLoadAllowedRoots().then(function() { + pageAiRenderOpencodeHostChrome(); + }).catch(function() { + pageAiRenderOpencodeHostChrome(); + }); + var opencodeDrawer = pageAiEnsureOpencodeHostDrawer(); + pageAiApplyDrawerWidth(opencodeDrawer); + opencodeDrawer.hidden = false; + pageUiState.pageAiOpen = true; + updatePageAiTriggerState(); + void pageAiLoadOpencodeStatus(); + return; + } pageUiState.pageAiAgentId = pageAiCurrentAgentId(); pageAiEnsureContextRefState(); pageAiLoadSessions(); @@ -2340,6 +3563,7 @@ export function createSidebarPageAiRuntime(context) { renderPageAiConversation(); renderPageAiProviderButtons(); renderPageAiControls(); + pageAiApplyBoardFirstShell(); var drawer = ensurePageAiDrawer(); pageAiApplyDrawerWidth(drawer); drawer.hidden = false; @@ -2352,6 +3576,7 @@ export function createSidebarPageAiRuntime(context) { return pageAiLoadRuntimeStatus().catch(function(error) { pageUiState.pageAiRuntimeStatusError = error instanceof Error ? error.message : String(error); renderPageAiControls(); + pageAiApplyBoardFirstShell(); return null; }); }); @@ -2360,6 +3585,7 @@ export function createSidebarPageAiRuntime(context) { }).catch(function(error) { pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); renderPageAiControls(); + pageAiApplyBoardFirstShell(); }); Promise.all([ pageAiLoadProfiles(), @@ -2369,21 +3595,26 @@ export function createSidebarPageAiRuntime(context) { pageAiLoadTools(), pageAiLoadGatewayHealth(), pageAiLoadAiPreferences(), + pageAiLoadBoardCatalog(), pageAiLoadAllowedRoots(), pageAiLoadBackendSessions(), pageAiLoadRuntimeStatus() ]).then(function() { renderPageAiControls(); + pageAiApplyBoardFirstShell(); }).then(function() { return pageAiCheckAndResumeActiveRun().catch(function(error) { pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); renderPageAiControls(); + pageAiApplyBoardFirstShell(); return null; }); }).then(function() { - return pageAiRestoreHermesSession(); + if (!pageAiBoardFirstEnabled()) return pageAiRestoreHermesSession(); + return null; }).then(function() { renderPageAiControls(); + pageAiApplyBoardFirstShell(); }).catch(function(error) { pageUiState.pageAiMessages.push({ role: 'assistant', @@ -2391,6 +3622,7 @@ export function createSidebarPageAiRuntime(context) { }); renderPageAiConversation(); renderPageAiControls(); + pageAiApplyBoardFirstShell(); }); } @@ -3005,6 +4237,10 @@ export function createSidebarPageAiRuntime(context) { } async function sendPageAiMessage(text) { + if (pageAiBoardFirstEnabled()) { + await sendPageAiBoardMessage(text); + return; + } var allowQueue = pageUiState.pageAiBusy || ['queued', 'running', 'tool_calling', 'pending', 'acp_pending'].indexOf(pageUiState.pageAiRunStatus) >= 0; if (pageUiState.pageAiBusy && !allowQueue) return; @@ -3367,6 +4603,12 @@ export function createSidebarPageAiRuntime(context) { closePageAiDrawer(); return true; } + var pageAiOpencodeFile = closestAction(event.target, '[data-page-ai-opencode-open-file]'); + if (pageAiOpencodeFile) { + event.preventDefault(); + pageAiOpenOpencodeChangedFile(pageAiOpencodeFile.getAttribute('data-page-ai-opencode-open-file') || ''); + return true; + } var pageAiSettings = closestAction(event.target, '[data-page-ai-action="open-hermes-settings"]'); if (pageAiSettings) { event.preventDefault(); @@ -3495,9 +4737,52 @@ export function createSidebarPageAiRuntime(context) { void sendPageAiMessage(message); } } + if (action === 'opencode-send') { + var opencodeInput = ensurePageAiDrawer().querySelector('[data-page-ai-opencode-input]'); + var opencodeText = opencodeInput instanceof HTMLTextAreaElement ? opencodeInput.value.trim() : ''; + if (opencodeText) { + opencodeInput.value = ''; + void pageAiSendOpencodeMessage(opencodeText).catch(function(error) { + pageUiState.pageAiOpencodeStatusError = error instanceof Error ? error.message : String(error); + pageUiState.pageAiOpencodeSending = false; + pageAiRenderOpencodeHostChrome(); + }); + } + } if (action === 'cancel-queued-run') { void pageAiCancelQueuedRun(pageAiAction.getAttribute('data-page-ai-queue-id')); } + if (action === 'opencode-refresh-status') { + void pageAiLoadOpencodeStatus(); + } + if (action === 'opencode-new-session') { + pageUiState.pageAiOpencodeSessionId = ''; + pageUiState.pageAiOpencodeBindingKey = ''; + pageUiState.pageAiOpencodeMessages = []; + pageUiState.pageAiOpencodeLocalMessages = []; + void pageAiEnsureOpencodeSessionBinding(true); + } + if (action === 'opencode-abort') { + void pageAiAbortOpencodeRun(); + } + if (action === 'opencode-refresh-current-page') { + pageAiRefreshOpencodeCurrentPage(); + } + if (action === 'opencode-insert-context') { + pageAiPostOpencodeBridgeMessage('insert-text', { text: pageAiOpencodeContextTextForInsert() }); + } + return true; + } + var pageAiSessionSelect = closestAction(event.target, '[data-page-ai-opencode-session-select]'); + if (pageAiSessionSelect) { + event.preventDefault(); + pageAiSelectOpencodeSession(pageAiSessionSelect.getAttribute('data-page-ai-opencode-session-select') || ''); + return true; + } + var pageAiPermission = closestAction(event.target, '[data-page-ai-opencode-permission]'); + if (pageAiPermission) { + event.preventDefault(); + void pageAiReplyOpencodePermission(pageAiPermission.getAttribute('data-page-ai-opencode-permission-id') || '', pageAiPermission.getAttribute('data-page-ai-opencode-permission') || 'once'); return true; } var pageAiTargetOption = closestAction(event.target, '[data-page-ai-target-option]'); @@ -3637,6 +4922,30 @@ export function createSidebarPageAiRuntime(context) { if (loc) pageAiOpenLocation(loc); return true; } + var pageAiBoardRunDetail = closestAction(event.target, '[data-page-ai-board-run-detail]'); + if (pageAiBoardRunDetail) { + event.preventDefault(); + pageUiState.pageAiBoardActiveDetailRunId = pageAiBoardRunDetail.getAttribute('data-page-ai-board-run-detail') || ''; + renderPageAiConversation(); + return true; + } + var pageAiCopyMessage = closestAction(event.target, '[data-page-ai-copy-message]'); + if (pageAiCopyMessage) { + event.preventDefault(); + var runId = pageAiCopyMessage.getAttribute('data-page-ai-copy-message') || ''; + var message = pageAiNormalizeArray(pageUiState.pageAiMessages).find(function(item) { return String(item && item.boardRunId || '') === runId; }); + if (navigator.clipboard && message) void navigator.clipboard.writeText(String(message.content || '')); + return true; + } + var pageAiRetryMessage = closestAction(event.target, '[data-page-ai-retry-message]'); + if (pageAiRetryMessage) { + event.preventDefault(); + var retryRunId = pageAiRetryMessage.getAttribute('data-page-ai-retry-message') || ''; + var retryMessage = pageAiNormalizeArray(pageUiState.pageAiMessages).find(function(item) { return String(item && item.boardRunId || '') === retryRunId; }); + var retryPrompt = String(retryMessage && retryMessage.retryPrompt || '').trim(); + if (retryPrompt) void sendPageAiMessage(retryPrompt); + return true; + } var pageAiSession = closestAction(event.target, '[data-page-ai-session]'); if (pageAiSession) { event.preventDefault(); @@ -3738,6 +5047,38 @@ export function createSidebarPageAiRuntime(context) { void pageAiSwitchProfile(pageAiProfileSelect.value); return true; } + var pageAiBoardWorker = closestAction(event.target, '[data-page-ai-board-worker]'); + if (pageAiBoardWorker instanceof HTMLSelectElement) { + pageUiState.pageAiBoardWorkerId = String(pageAiBoardWorker.value || pageAiBoardDefaultWorkerId()).trim() || pageAiBoardDefaultWorkerId(); + pageUiState.pageAiBoardModelOverride = pageAiBoardSelectedModelOverride(); + pageAiPersistAiPreference('board.worker_preset_id', pageUiState.pageAiBoardWorkerId); + pageAiPersistAiPreference('board.model_override', pageUiState.pageAiBoardModelOverride); + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + return true; + } + var pageAiBoardModel = closestAction(event.target, '[data-page-ai-board-model]'); + if (pageAiBoardModel instanceof HTMLSelectElement) { + pageUiState.pageAiBoardModelOverride = String(pageAiBoardModel.value || pageAiBoardSelectedModelOverride()).trim() || pageAiBoardSelectedModelOverride(); + pageAiPersistAiPreference('board.model_override', pageUiState.pageAiBoardModelOverride); + var current = pageAiCurrentSession(); + if (current) { + current.modelOverride = pageUiState.pageAiBoardModelOverride; + current.updatedAt = Date.now(); + } + pageAiPersistSessions(); + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + return true; + } + var pageAiBoardWorkflow = closestAction(event.target, '[data-page-ai-board-workflow]'); + if (pageAiBoardWorkflow instanceof HTMLSelectElement) { + pageUiState.pageAiBoardWorkflowId = String(pageAiBoardWorkflow.value || pageAiBoardDefaultWorkflowId()).trim() || pageAiBoardDefaultWorkflowId(); + pageAiPersistAiPreference('board.workflow_id', pageUiState.pageAiBoardWorkflowId); + renderPageAiControls(); + pageAiApplyBoardFirstShell(); + return true; + } var pageAiSkillSourceSelect = closestAction(event.target, '[data-page-ai-skill-source-select]'); if (pageAiSkillSourceSelect instanceof HTMLSelectElement) { pageAiSetSkillSource(pageAiSkillSourceSelect.value); @@ -3783,6 +5124,19 @@ export function createSidebarPageAiRuntime(context) { document.addEventListener('click', function(event) { handlePageAiClick(event, helpers); }); + document.addEventListener('submit', function(event) { + var form = event.target && event.target.closest ? event.target.closest('[data-page-ai-opencode-composer]') : null; + if (!form) return; + event.preventDefault(); + var input = form.querySelector('[data-page-ai-opencode-input]'); + var text = input instanceof HTMLTextAreaElement ? input.value.trim() : ''; + if (!text) return; + input.value = ''; + void pageAiSendOpencodeMessage(text).catch(function(error) { + pageUiState.pageAiOpencodeStatusError = error instanceof Error ? error.message : String(error); + pageAiRenderOpencodeHostChrome(); + }); + }); document.addEventListener('pointerdown', function(event) { handlePageAiPointerDown(event, helpers); }); @@ -3856,6 +5210,11 @@ export function createSidebarPageAiRuntime(context) { pageAiSetContextPopoverOpen: (...args) => pageAiSetContextPopoverOpen(...args), pageAiSetAgentPopoverOpen: (...args) => pageAiSetAgentPopoverOpen(...args), pageAiLoadAllowedRoots: (...args) => pageAiLoadAllowedRoots(...args), + pageAiEnsureOpencodeHostDrawer: (...args) => pageAiEnsureOpencodeHostDrawer(...args), + pageAiLoadOpencodeStatus: (...args) => pageAiLoadOpencodeStatus(...args), + pageAiLoadOpencodeDiff: (...args) => pageAiLoadOpencodeDiff(...args), + pageAiOpenOpencodeChangedFile: (...args) => pageAiOpenOpencodeChangedFile(...args), + pageAiRefreshOpencodeCurrentPage: (...args) => pageAiRefreshOpencodeCurrentPage(...args), updatePageAiTriggerState: (...args) => updatePageAiTriggerState(...args) }; } diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js index 8a509ee8..9ea8e4b2 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js @@ -82,6 +82,7 @@ export function createSidebarPageAiSessionRuntime(context) { profile = profileId; } return { + schema: String(session && session.schema || '').trim(), id: String(session && session.id || '').trim() || pageAiNewSession().id, title: String(session && session.title || '').trim() || '新会话', agentId: agentId, @@ -97,12 +98,18 @@ export function createSidebarPageAiSessionRuntime(context) { shareId: String(session && (session.shareId || session.share_id) || '').trim(), acpSessionId: String(session && (session.acpSessionId || session.acp_session_id) || '').trim(), runId: String(session && (session.runId || session.run_id) || '').trim(), + boardRunId: String(session && (session.boardRunId || session.board_run_id || session.runId || session.run_id) || '').trim(), + workflowId: String(session && (session.workflowId || session.workflow_id) || '').trim(), + workerPresetId: String(session && (session.workerPresetId || session.worker_preset_id) || '').trim(), + modelOverride: String(session && (session.modelOverride || session.model_override) || '').trim(), + receiptId: String(session && (session.receiptId || session.receipt_id) || '').trim(), + boardRuns: session && session.boardRuns && typeof session.boardRuns === 'object' ? session.boardRuns : {}, status: String(session && session.status || '').trim(), runtimeMode: String(session && (session.runtimeMode || session.runtime_mode) || '').trim(), replaySeen: Boolean(session && session.replaySeen), usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null, preview: String(session && session.preview || '').trim(), - messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : [] + messages: Array.isArray(session && session.messages) ? session.messages.slice(-300).map(function(message) { return Object.assign({}, message); }) : [] }; }) .sort(function(a, b) { @@ -208,6 +215,15 @@ export function createSidebarPageAiSessionRuntime(context) { var storageVersion = Number(parsed && parsed.version || 0); if (storageVersion >= sessionStorageVersion && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime; if (activeProfile) pageAiSetActiveProfile(activeProfile); + var storedSessions = pageAiNormalizeSessions(parsed && parsed.sessions); + if (storageVersion >= sessionStorageVersion && storedSessions.length) { + pageUiState.pageAiSessions = storedSessions; + var activeSessionId = String(parsed && parsed.activeSessionId || '').trim(); + pageUiState.pageAiActiveSessionId = storedSessions.some(function(session) { return session.id === activeSessionId; }) ? activeSessionId : storedSessions[0].id; + var active = pageAiCurrentSession(); + pageUiState.pageAiMessages = active && Array.isArray(active.messages) ? active.messages.slice() : []; + return; + } } catch (_) {} var fresh = pageAiNewSession(); pageUiState.pageAiSessions = [fresh]; @@ -443,15 +459,16 @@ export function createSidebarPageAiSessionRuntime(context) { } function pageAiPersistSessions() { + pageAiSyncCurrentSessionMessages(); doc.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes'); doc.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey()); try { - pageAiSyncCurrentSessionMessages(); win.localStorage.setItem(pageAiStorageKey(), JSON.stringify({ version: sessionStorageVersion, activeSessionId: pageUiState.pageAiActiveSessionId, activeProfileName: pageAiCurrentProfile(), - activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix' + activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix', + sessions: pageAiNormalizeArray(pageUiState.pageAiSessions).slice(0, 20) })); } catch (_) {} } @@ -603,6 +620,12 @@ export function createSidebarPageAiSessionRuntime(context) { session.profileId = pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : ''; session.profile = runProfile; session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix'; + if (String(session.source || '') === 'board') { + session.schema = session.schema || 'mnote.page_ai.session.v2'; + session.workerPresetId = pageUiState.pageAiBoardWorkerId || session.workerPresetId || ''; + session.workflowId = pageUiState.pageAiBoardWorkflowId || session.workflowId || ''; + session.modelOverride = pageUiState.pageAiBoardModelOverride || session.modelOverride || ''; + } session.updatedAt = Date.now(); } @@ -611,6 +634,14 @@ export function createSidebarPageAiSessionRuntime(context) { if (!session) return; pageUiState.pageAiActiveSessionId = session.id; if (session.agentId) pageUiState.pageAiAgentId = pageAiNormalizeAgentId(session.agentId); + if (session.source === 'board') { + if (session.workerPresetId) pageUiState.pageAiBoardWorkerId = session.workerPresetId; + if (session.workflowId) pageUiState.pageAiBoardWorkflowId = session.workflowId; + if (session.modelOverride) pageUiState.pageAiBoardModelOverride = session.modelOverride; + if (session.boardRuns && typeof session.boardRuns === 'object') { + pageUiState.pageAiBoardRunDetails = Object.assign({}, pageUiState.pageAiBoardRunDetails || {}, session.boardRuns); + } + } if (session.acpRuntime) pageUiState.pageAiAcpRuntime = session.acpRuntime; if (session.profileId || session.profile) pageAiSetActiveProfile(session.profileId || session.profile); pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : []; diff --git a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js index 37b5e4bb..724a082c 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js @@ -464,7 +464,7 @@ export function createSidebarPageSettingsRuntime(context) { '
    ' + '
    ' + '
    页面历史
    当前会话内最近保存的 15 个快照。
    ' + - '' + + '' + '
    ' + '
    ' + '
    '; @@ -527,7 +527,7 @@ export function createSidebarPageSettingsRuntime(context) { '