From 84065a7f329b7e45705b7cf93f4695cf0e0d7465 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:18:55 +0800 Subject: [PATCH 01/17] ci: upload Electron desktop artifacts to R2 for CN downloads Add build-desktop dependency to upload-to-hub job and include Markus-*.dmg, *.exe, *.AppImage in the R2 upload loop so the Hub /api/releases/latest API can serve desktop app downloads from the CN-optimized R2 endpoint. Co-authored-by: Cursor --- .github/workflows/publish.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 159e973c..50e2d67b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -504,16 +504,16 @@ jobs: upload-to-hub: runs-on: ubuntu-latest - needs: [publish-npm, build-binaries, github-release] + needs: [publish-npm, build-binaries, build-desktop, github-release] if: needs.publish-npm.outputs.is_prerelease == 'false' steps: - - name: Download all binary artifacts + - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts merge-multiple: true - - name: Upload binaries directly to R2 + - name: Upload binaries and desktop apps to R2 env: AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} @@ -523,7 +523,7 @@ jobs: run: | set -e uploaded=0 - for file in artifacts/markus-setup-* artifacts/markus-v${VERSION}-*; do + for file in artifacts/markus-setup-* artifacts/markus-v${VERSION}-* artifacts/Markus-*.dmg artifacts/Markus-*.exe artifacts/Markus-*.AppImage; do [ -f "$file" ] || continue filename=$(basename "$file") echo "Uploading $filename to R2..." @@ -534,7 +534,7 @@ jobs: done if [ "$uploaded" -eq 0 ]; then - echo "::error::No binary artifacts found to upload" + echo "::error::No artifacts found to upload" exit 1 fi From 02b47d218ee62aaf26e08f82efd12b8ff26664f9 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:22:38 +0800 Subject: [PATCH 02/17] release v0.8.4-rc.0: ci: upload desktop artifacts to R2, mac landing page downloads Electron dmg --- package.json | 2 +- packages/a2a/package.json | 2 +- packages/cli/package.json | 2 +- packages/comms/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/gui/package.json | 2 +- packages/org-manager/package.json | 2 +- packages/remote/package.json | 2 +- packages/shared/package.json | 2 +- packages/storage/package.json | 2 +- packages/web-ui/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 956f9446..81ee3a73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "markus", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "description": "AI Native Digital Employee Platform", diff --git a/packages/a2a/package.json b/packages/a2a/package.json index 33c3ea48..bbc5b4ed 100644 --- a/packages/a2a/package.json +++ b/packages/a2a/package.json @@ -1,6 +1,6 @@ { "name": "@markus/a2a", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 00badd3b..a19d5ddb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@markus-global/cli", - "version": "0.8.3", + "version": "0.8.4-rc.0", "description": "Markus — AI Digital Workforce Platform", "type": "module", "license": "AGPL-3.0-or-later", diff --git a/packages/comms/package.json b/packages/comms/package.json index d16d94b6..4ea8cfbe 100644 --- a/packages/comms/package.json +++ b/packages/comms/package.json @@ -1,6 +1,6 @@ { "name": "@markus/comms", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 2a79d5cd..0eedc1ee 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@markus/core", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 2731bb07..56df3193 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@markus/desktop", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "productName": "Markus", "author": "Markus Global", diff --git a/packages/gui/package.json b/packages/gui/package.json index a401cc8e..5bf0df48 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -1,6 +1,6 @@ { "name": "@markus/gui", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/org-manager/package.json b/packages/org-manager/package.json index 27379ab4..c8e5f02a 100644 --- a/packages/org-manager/package.json +++ b/packages/org-manager/package.json @@ -1,6 +1,6 @@ { "name": "@markus/org-manager", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/remote/package.json b/packages/remote/package.json index aa3fb29a..fb7b1906 100644 --- a/packages/remote/package.json +++ b/packages/remote/package.json @@ -1,6 +1,6 @@ { "name": "@markus/remote", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 7fbd0150..40e29ad1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@markus/shared", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/storage/package.json b/packages/storage/package.json index 2b90b9fb..0b13146c 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@markus/storage", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 79a7efb3..72a201fb 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@markus/web-ui", - "version": "0.8.3", + "version": "0.8.4-rc.0", "private": true, "type": "module", "exports": { From 91c101240ed07d16790b92111531cc94e3b98792 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:33:04 +0800 Subject: [PATCH 03/17] =?UTF-8?q?fix(ci):=20desktop=20build=20failures=20?= =?UTF-8?q?=E2=80=94=20Apple=20notarization=20env=20var=20and=20Linux=20ex?= =?UTF-8?q?ecutableName?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add APPLE_APP_SPECIFIC_PASSWORD env var (electron-builder requires this for notarization, not APPLE_ID_PASSWORD) - Set linux.executableName to 'markus' in electron-builder.yml to avoid the scoped package name @markus/desktop producing invalid '@markusdesktop' Co-authored-by: Cursor --- .github/workflows/publish.yml | 1 + packages/desktop/electron-builder.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 50e2d67b..91e1000c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -306,6 +306,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Cleanup Apple keychain diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index e8226eb3..7e659b29 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -64,6 +64,7 @@ nsis: createStartMenuShortcut: true linux: + executableName: markus target: - target: AppImage arch: [x64] From 40cc9bcee6dab7c2da87fb1ba5262be5daab0786 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:41:21 +0800 Subject: [PATCH 04/17] docs: backfill release logs for v0.7.9 through v0.8.4 Co-authored-by: Cursor --- RELEASELOG.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/RELEASELOG.md b/RELEASELOG.md index c18dedea..4ae6fa44 100644 --- a/RELEASELOG.md +++ b/RELEASELOG.md @@ -1,5 +1,206 @@ # Release Log +## v0.8.4 + +Electron 桌面应用;智能模型路由(多模态模型发现 + 区域提供商拆分 + 路由 UI);Free 计划更名 Community 并提升上限;CI Desktop 构建修复;R2 上传 Electron 产物。 + +### New Features + +- **Electron 桌面应用** — 全新 `packages/desktop`,支持 macOS/Windows/Linux;启动闪屏、系统通知(仅关键事件)、应用菜单中英双语 i18n、macOS 签名与公证 +- **智能模型路由** — 动态提供商 schema 发现、多模态模型自动检测(所有提供商)、区域提供商拆分、路由 UI 与定价同步 +- **Community 计划** — Free 计划更名为 Community,提升 Agent 和团队数量上限 + +### Bug Fixes + +- **修复 Desktop 构建 CI** — 添加 `APPLE_APP_SPECIFIC_PASSWORD` 环境变量解决 macOS 公证失败;Linux `executableName` 设为 `markus` 解决 AppImage 包名含 `@` 非法字符 +- **修复 R2 上传遗漏 Desktop 产物** — `upload-to-hub` 增加 `build-desktop` 依赖,上传 `.dmg`/`.exe`/`.AppImage` 到 R2 +- **修复暗色主题与 traffic light** — 冷中性暗色主题、macOS 窗口交通灯动态定位 +- **修复交付物时间戳与聊天滚动** — deliverable 时间戳稳定性、聊天滚动、桌面通知导航 +- **修复飞书 dirname 未定义** — Lark adapter `__dirname` 引用修复 +- **修复 Agent 初始状态** — 新 Agent 初始状态改为 offline 而非 idle;desired-state 与 runtime-state 分离确保重启持久化 +- **修复模型路由多模态兼容** — 多模态 provider API 兼容性问题、模型 ID 解析、定价同步 +- **修复深度审计发现的 9 项问题** — T-001/006/011/014/015/017/019/022/026 +- **修复 MCP client EPIPE 与 mock 兼容** — stdin.on() 空指针保护、登录测试竞态、CI 测试隔离 ~/.markus + +### Improvements + +- **Desktop 通知精简** — 仅显示关键事件通知,减少噪音 +- **Desktop 闪屏改用 Logo** — 启动闪屏使用 Logo 图片替代文字 +- **单测覆盖率 80%+** — 深度代码审计与测试补全 + +### Stats + +- 307 files changed, +52,928 / −3,266 lines + +--- + +## v0.8.3 + +防止通信适配器连接失败导致服务崩溃;侧边栏设置按钮简化;登录提示可见性修复。 + +### Bug Fixes + +- **修复通信适配器崩溃** — comm adapter(飞书/Slack/WhatsApp)连接失败时捕获异常,防止整个服务进程退出 +- **修复侧边栏设置按钮** — 简化设置入口,修复登录提示在特定条件下不可见 + +### Stats + +- 15 files changed, +41 / −37 lines + +--- + +## v0.8.2 + +飞书集成全链路(QR 码注册、消息路由、通知转发);工作流系统(Phase 1);Codex OAuth 支持;暗色主题重设计;聊天输入与侧边栏优化。 + +### New Features + +- **飞书集成** — 一键扫码注册飞书应用、消息路由至 Secretary、通知转发、markus.json 作为凭据唯一来源、私信回退提示 +- **工作流系统 Phase 1** — 工作流创建与运行、自动启动任务、Secretary 审批回退、搜索/mention/实体渲染集成 +- **OpenAI Codex OAuth** — Codex OAuth 代理与 token 刷新、Settings UI 配置 +- **Hub 分享可见性** — Hub 分享状态持久化到 localStorage,新增可见性级别与组织资产标签 + +### Bug Fixes + +- **修复 OAuth 登录竞态** — 解决 OAuth 登录过程中的竞态条件,补充缺失的 i18n key +- **修复 License 同步与引导简化** — License 同步流程优化,Onboarding 简化 +- **修复 Markdown 实体链接** — 正确渲染被反引号包裹的实体链接 +- **修复工作流持久化与重试** — 工作流验证、持久化、重试机制加固 + +### Improvements + +- **暗色主题重设计** — Codex 风格暖中性色调 +- **聊天输入框与侧边栏优化** — 输入框交互改进,侧边栏折叠优化,未读计数 +- **交付物页面聊天面板** — 支持上下文感知的 @mention 和选择工具栏 +- **Work 页面团队分组** — Agent 筛选栏替换为团队分组视图 + +### Stats + +- 101 files changed, +12,620 / −1,484 lines + +--- + +## v0.8.1 + +Windows 安装包修复;企业版试用期调整;UI 小改进。 + +### Bug Fixes + +- **修复 Windows 安装包** — 安装器自动关闭占用进程,chrome:// 按钮回退方案,引导清单移除搜索步骤 +- **调整企业版试用期** — 试用期从 3 天延长至 7 天 + +### Stats + +- 16 files changed, +56 / −45 lines + +--- + +## v0.8.0 + +企业版授权系统(Hub OAuth、组织管理与计费);License 限额看板;工具调用统计统一。 + +### New Features + +- **企业版授权系统** — 基于 Hub OAuth 的企业授权,组织管理与计费 UI,License 激活后自动刷新,Hub 组织成员数展示 +- **License 限额看板** — 统一从 Agent 指标提取工具调用计数,超限横幅提醒 + +### Bug Fixes + +- **修复 Hub 重定向 Auth Header 丢失** — 保留 Hub 重定向时的认证头 +- **修复本地登录路径** — 恢复 Hub 连接后的本地登录入口,版本横幅改为指向网站下载 + +### Stats + +- 48 files changed, +2,802 / −306 lines + +--- + +## v0.7.14 + +完成标记会话内续接修复;注意力重试逻辑改进;聊天滚动 UX 优化。 + +### Bug Fixes + +- **修复完成标记重试** — 完成标记(completion marker)通过会话内续接重试,而非重新创建会话 +- **修复注意力重试逻辑** — attention controller 重试逻辑改进,避免无效重试 +- **修复聊天滚动 UX** — 聊天滚动行为优化 + +### Stats + +- 14 files changed, +144 / −52 lines + +--- + +## v0.7.13 + +修复 Windows 打开 Chrome 扩展页面的启动命令。 + +### Bug Fixes + +- **修复 Windows Chrome 扩展页面** — 修正 Windows 上打开 `chrome://extensions` 的 `start` 命令参数 + +### Stats + +- 12 files changed, +12 / −12 lines (version bump) + +--- + +## v0.7.12 + +修复二进制构建中 node-datachannel 版本不稳定问题。 + +### Bug Fixes + +- **固定 node-datachannel 版本** — 二进制构建中将 node-datachannel 固定为 0.12.0,避免不兼容的版本更新 + +### Stats + +- 12 files changed, +12 / −12 lines (version bump) + +--- + +## v0.7.11 + +修复 Chrome Extension 打包脚本跨平台路径问题。 + +### Bug Fixes + +- **修复跨平台路径解析** — Chrome Extension `pack.mjs` 使用 `fileURLToPath` 替代直接路径操作,修复 Windows 路径兼容性 + +### Stats + +- 12 files changed, +15 / −14 lines + +--- + +## v0.7.10 + +CI 改为直接上传 release 二进制到 R2,不再走 API 中转。 + +### Improvements + +- **R2 直传** — CI 发布流程改为直接上传二进制到 Cloudflare R2,移除 API 中转环节,提升上传可靠性 + +### Stats + +- 12 files changed, +34 / −20 lines + +--- + +## v0.7.9 + +安装脚本添加 markus.global 镜像回退,提升中国区安装体验。 + +### Improvements + +- **安装脚本镜像回退** — `install.sh` 和 `install.ps1` 添加 markus.global 作为下载源回退,GitHub 不可达时自动切换 + +### Stats + +- 14 files changed, +85 / −31 lines + +--- + ## v0.7.8 LiteLLM 模型目录集成(动态模型选择);聊天与工作台 UX 全面优化;Windows 安装包修复;SSE 断连韧性增强;执行时间线 i18n 支持。 From 8f6936c224329de4b4ef4a5973296e9da8e3dd31 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:48:46 +0800 Subject: [PATCH 05/17] release v0.8.4-rc.1: fix desktop CI builds, backfill release logs Co-authored-by: Cursor --- package.json | 2 +- packages/a2a/package.json | 2 +- packages/cli/package.json | 2 +- packages/comms/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/gui/package.json | 2 +- packages/org-manager/package.json | 2 +- packages/remote/package.json | 2 +- packages/shared/package.json | 2 +- packages/storage/package.json | 2 +- packages/web-ui/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 81ee3a73..ff898d36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "markus", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "description": "AI Native Digital Employee Platform", diff --git a/packages/a2a/package.json b/packages/a2a/package.json index bbc5b4ed..2360fbba 100644 --- a/packages/a2a/package.json +++ b/packages/a2a/package.json @@ -1,6 +1,6 @@ { "name": "@markus/a2a", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index a19d5ddb..851ebf4a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@markus-global/cli", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "description": "Markus — AI Digital Workforce Platform", "type": "module", "license": "AGPL-3.0-or-later", diff --git a/packages/comms/package.json b/packages/comms/package.json index 4ea8cfbe..4ffc4644 100644 --- a/packages/comms/package.json +++ b/packages/comms/package.json @@ -1,6 +1,6 @@ { "name": "@markus/comms", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index 0eedc1ee..d668f086 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@markus/core", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 56df3193..3196f6d7 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@markus/desktop", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "productName": "Markus", "author": "Markus Global", diff --git a/packages/gui/package.json b/packages/gui/package.json index 5bf0df48..94bb7cb4 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -1,6 +1,6 @@ { "name": "@markus/gui", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/org-manager/package.json b/packages/org-manager/package.json index c8e5f02a..5975290f 100644 --- a/packages/org-manager/package.json +++ b/packages/org-manager/package.json @@ -1,6 +1,6 @@ { "name": "@markus/org-manager", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/remote/package.json b/packages/remote/package.json index fb7b1906..ee9a0b52 100644 --- a/packages/remote/package.json +++ b/packages/remote/package.json @@ -1,6 +1,6 @@ { "name": "@markus/remote", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 40e29ad1..73c7ba2c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@markus/shared", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/storage/package.json b/packages/storage/package.json index 0b13146c..9ba47656 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@markus/storage", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "main": "dist/index.js", diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 72a201fb..e992898c 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@markus/web-ui", - "version": "0.8.4-rc.0", + "version": "0.8.4-rc.1", "private": true, "type": "module", "exports": { From 416e2fc5260289329e84be5bad35ef890dc2f61b Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 17:56:53 +0800 Subject: [PATCH 06/17] fix(desktop): add author email and homepage for Linux .deb build electron-builder requires homepage, author email, and maintainer metadata when building .deb packages. AppImage was unaffected. Co-authored-by: Cursor --- packages/desktop/package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 3196f6d7..1da75d4a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -3,7 +3,11 @@ "version": "0.8.4-rc.1", "private": true, "productName": "Markus", - "author": "Markus Global", + "author": { + "name": "Markus Global", + "email": "team@markus.global" + }, + "homepage": "https://www.markus.global", "description": "Markus Desktop App — Electron wrapper for the AI Digital Workforce Platform", "type": "module", "main": "dist/main.js", From 9124d22ae851b78b7e440d805137c3a97a7b6c68 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 19 Jun 2026 19:46:00 +0800 Subject: [PATCH 07/17] refactor: eliminate branch deliverable hack, unify data sources - Add task.completionSummary field to replace synthetic branch deliverables - Remove branch injection in agent-manager, pass completionSummary directly - Update submitForReview to persist completionSummary to DB - Switch Work page deliverables tab to query unified table via API - Update /api/tasks/deliverables to aggregate from unified table - Remove migrateFromTasks startup migration, add cleanupLegacyRows - Add migrateBranchToCompletionSummary one-time migration - Show full datetime in Deliverables page metadata Co-authored-by: Cursor --- packages/cli/src/commands/start.ts | 7 +- packages/core/src/agent-manager.ts | 18 ++-- packages/org-manager/src/api-server.ts | 51 +++++++--- .../org-manager/src/deliverable-service.ts | 71 +++----------- packages/org-manager/src/task-service.ts | 92 ++++++++++++++----- .../test/deliverable-service.test.ts | 20 +--- packages/shared/src/types/governance.ts | 1 + packages/shared/src/types/task.ts | 2 + packages/storage/src/sqlite-storage.ts | 12 +++ packages/storage/src/types.ts | 2 + packages/web-ui/src/pages/Deliverables.tsx | 4 +- packages/web-ui/src/pages/Work.tsx | 26 ++++-- 12 files changed, 168 insertions(+), 138 deletions(-) diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 36c79869..707c44c2 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -597,10 +597,9 @@ async function startServerCore( const deliverableService = new DeliverableService(storage?.deliverableRepo); await deliverableService.load(); - // One-time migration: sync existing task.deliverables into the unified deliverables table - const allTasks = taskService.listTasks({ orgId: 'default' }); - await deliverableService.migrateFromTasks(allTasks); - await deliverableService.deduplicateByReference(); + // One-time migrations + await taskService.migrateBranchToCompletionSummary(); + await deliverableService.cleanupLegacyRows(); const reportService = new ReportService(taskService, billingService, auditService, knowledgeService); const _trustService = new TrustService(); diff --git a/packages/core/src/agent-manager.ts b/packages/core/src/agent-manager.ts index 29f9ce12..7c646fbc 100644 --- a/packages/core/src/agent-manager.ts +++ b/packages/core/src/agent-manager.ts @@ -232,7 +232,7 @@ export interface TaskServiceBridge { addSubtask(taskId: string, title: string): { id: string; title: string; status: string }; completeSubtask(taskId: string, subtaskId: string): { id: string; title: string; status: string }; getSubtasks?(taskId: string): Array<{ id: string; title: string; status: string }>; - submitForReview(taskId: string, deliverables: Array<{ type: string; reference: string; summary: string; diffStats?: unknown; testResults?: unknown }>, reviewerId?: string): Promise<{ id: string; status: string }>; + submitForReview(taskId: string, deliverables: Array<{ type: string; reference: string; summary: string; diffStats?: unknown; testResults?: unknown }>, reviewerId?: string, completionSummary?: string): Promise<{ id: string; status: string }>; requestRevision(taskId: string, reason: string, author?: string): Promise<{ id: string; title: string; status: string }>; findDuplicateTasks?(orgId: string): Array<{ group: string; tasks: Array<{ id: string; title: string; status: string; createdAt: string }> }>; cleanupDuplicateTasks?(orgId: string): { cancelledIds: string[]; count: number }; @@ -1247,10 +1247,8 @@ export class AgentManager { if (!task) throw new Error(`Task not found: ${taskId}`); const reviewerId = (task as Record).reviewerId as string | undefined; const _validTypes = new Set(['file', 'directory']); - const deliverables: Array<{ type: string; reference: string; summary: string }> = [{ - type: 'branch', reference: `task/${taskId}`, - summary: `${summary}${knownIssues ? `\n\nKnown issues: ${knownIssues}` : ''}`, - }]; + const completionSummary = `${summary}${knownIssues ? `\n\nKnown issues: ${knownIssues}` : ''}`; + const deliverables: Array<{ type: string; reference: string; summary: string }> = []; if (Array.isArray(inputDeliverables)) { for (const d of inputDeliverables) { if (d?.reference) { @@ -1261,7 +1259,7 @@ export class AgentManager { } } } - return ts.submitForReview(taskId, deliverables, reviewerId); + return ts.submitForReview(taskId, deliverables, reviewerId, completionSummary); }, proposeRequirement: this.requirementService ? async params => { @@ -2041,10 +2039,8 @@ export class AgentManager { if (!task) throw new Error(`Task not found: ${taskId}`); const reviewerId = (task as Record).reviewerId as string | undefined; const _validTypes = new Set(['file', 'directory']); - const deliverables: Array<{ type: string; reference: string; summary: string }> = [{ - type: 'branch', reference: `task/${taskId}`, - summary: `${summary}${knownIssues ? `\n\nKnown issues: ${knownIssues}` : ''}`, - }]; + const completionSummary = `${summary}${knownIssues ? `\n\nKnown issues: ${knownIssues}` : ''}`; + const deliverables: Array<{ type: string; reference: string; summary: string }> = []; if (Array.isArray(inputDeliverables)) { for (const d of inputDeliverables) { if (d?.reference) { @@ -2055,7 +2051,7 @@ export class AgentManager { } } } - return ts.submitForReview(taskId, deliverables, reviewerId); + return ts.submitForReview(taskId, deliverables, reviewerId, completionSummary); }, proposeRequirement: this.requirementService ? async params => { diff --git a/packages/org-manager/src/api-server.ts b/packages/org-manager/src/api-server.ts index 18547c8b..ab032b14 100644 --- a/packages/org-manager/src/api-server.ts +++ b/packages/org-manager/src/api-server.ts @@ -3493,20 +3493,43 @@ export class APIServer { if (path === '/api/tasks/deliverables' && req.method === 'GET') { const projectId = url.searchParams.get('projectId') ?? undefined; - const all = this.taskService.listTasks({ projectId }); - const items = all - .filter(t => t.deliverables && t.deliverables.length > 0) - .map(t => ({ - taskId: t.id, - taskTitle: t.title, - taskStatus: t.status, - projectId: t.projectId, - requirementId: t.requirementId, - assignedAgentId: t.assignedAgentId, - updatedAt: t.updatedAt, - deliverables: t.deliverables, - })); - this.json(res, 200, { items }); + if (this.deliverableService) { + const { results } = this.deliverableService.search({ projectId, limit: 500 }); + const grouped = new Map(); + for (const d of results) { + if (!d.taskId) continue; + if (!grouped.has(d.taskId)) { + const task = this.taskService.getTask(d.taskId); + grouped.set(d.taskId, { + taskId: d.taskId, + taskTitle: task?.title ?? '', + taskStatus: task?.status ?? '', + projectId: task?.projectId, + requirementId: task?.requirementId, + assignedAgentId: task?.assignedAgentId, + updatedAt: task?.updatedAt, + deliverables: [], + }); + } + grouped.get(d.taskId)!.deliverables.push(d); + } + this.json(res, 200, { items: [...grouped.values()] }); + } else { + const all = this.taskService.listTasks({ projectId }); + const items = all + .filter(t => t.deliverables && t.deliverables.length > 0) + .map(t => ({ + taskId: t.id, + taskTitle: t.title, + taskStatus: t.status, + projectId: t.projectId, + requirementId: t.requirementId, + assignedAgentId: t.assignedAgentId, + updatedAt: t.updatedAt, + deliverables: t.deliverables, + })); + this.json(res, 200, { items }); + } return; } diff --git a/packages/org-manager/src/deliverable-service.ts b/packages/org-manager/src/deliverable-service.ts index 001849e7..820376e4 100644 --- a/packages/org-manager/src/deliverable-service.ts +++ b/packages/org-manager/src/deliverable-service.ts @@ -1,6 +1,6 @@ import { existsSync, cpSync, mkdirSync } from 'node:fs'; import { join, basename } from 'node:path'; -import { createLogger, generateId, type Deliverable, type TaskDeliverable, type Task } from '@markus/shared'; +import { createLogger, generateId, type Deliverable } from '@markus/shared'; import type { DeliverableRepo } from '@markus/storage'; import type { WSBroadcaster } from './ws-server.ts'; const log = createLogger('deliverable-service'); @@ -405,62 +405,23 @@ export class DeliverableService { } /** - * One-time migration: scan existing tasks and create Deliverable entries - * for any task.deliverables that don't yet have a corresponding row. - * Also cleans up any legacy "branch"-type deliverables by marking them outdated. + * Clean up legacy migration markers and branch-type deliverables from the table. + * Safe to call on startup — removes only housekeeping rows. */ - async migrateFromTasks(tasks: Task[]): Promise { - // Clean up legacy branch-type deliverables (branch is task metadata, not a standalone deliverable) - let branchCleaned = 0; + async cleanupLegacyRows(): Promise { + let cleaned = 0; for (const [id, d] of this.cache) { - if ((d.type as string) === 'branch' && d.status !== 'outdated') { - d.status = 'outdated'; - d.updatedAt = new Date().toISOString(); - await this.repo?.update(id, { status: 'outdated' }); - branchCleaned++; - } - } - if (branchCleaned > 0) { - log.info('Cleaned up legacy branch-type deliverables', { count: branchCleaned }); - } - - // Query ALL task IDs that have ever had deliverables (including outdated/deleted) - // so we don't re-import items the user already removed. - const existingTaskIds = this.repo - ? await this.repo.listTaskIdsWithDeliverables() - : new Set([...this.cache.values()].map(d => d.taskId).filter(Boolean) as string[]); - - let migrated = 0; - for (const task of tasks) { - if (!task.deliverables?.length) continue; - if (existingTaskIds.has(task.id)) continue; - - for (const d of task.deliverables) { - if (d.type === 'branch') continue; - try { - await this.create({ - type: this.mapTaskDeliverableType(d.type), - title: d.summary?.slice(0, 200) || d.reference, - summary: d.summary || '', - reference: d.reference, - taskId: task.id, - agentId: task.assignedAgentId, - projectId: task.projectId, - requirementId: task.requirementId, - diffStats: d.diffStats, - testResults: d.testResults, - }); - migrated++; - } catch (err) { - log.warn('Failed to migrate task deliverable', { taskId: task.id, ref: d.reference, error: String(err) }); - } + const isMigrationMarker = d.title === '[migration-processed]' && d.status === 'outdated'; + const isBranchType = (d.type as string) === 'branch'; + if (isMigrationMarker || isBranchType) { + this.cache.delete(id); + await this.repo?.delete(id); + cleaned++; } } - - if (migrated > 0) { - log.info('Migrated task deliverables to unified table', { migrated }); + if (cleaned > 0) { + log.info('Cleaned up legacy deliverable rows', { count: cleaned }); } - return migrated; } private parseTags(raw: unknown): string[] { @@ -475,12 +436,6 @@ export class DeliverableService { return []; } - private mapTaskDeliverableType(type: TaskDeliverable['type']): Deliverable['type'] { - switch (type) { - case 'file': return 'file'; - default: return 'file'; - } - } private rowToDeliverable(r: { id: string; diff --git a/packages/org-manager/src/task-service.ts b/packages/org-manager/src/task-service.ts index 6ffd517b..41e06206 100644 --- a/packages/org-manager/src/task-service.ts +++ b/packages/org-manager/src/task-service.ts @@ -1082,13 +1082,18 @@ export class TaskService { lines.push(`- ${note.slice(0, PROMPT_DEP_NOTE_CHARS)}`); } } + if (depTask.completionSummary) { + lines.push(`**Completion Summary:** ${depTask.completionSummary}`); + } if (depTask.deliverables?.length) { - lines.push('**Deliverables (review these for background context):**'); - for (const d of depTask.deliverables) { - const refInfo = d.type === 'file' ? ` — File: \`${d.reference}\` (use \`file_read\` to inspect)` : - d.type === 'branch' ? ` [branch: ${d.reference}]` : - d.reference ? ` — ref: \`${d.reference}\`` : ''; - lines.push(`- ${d.summary ?? '(no summary)'}${refInfo}`); + const files = depTask.deliverables.filter(d => d.type !== 'branch' && d.reference); + if (files.length > 0) { + lines.push('**Deliverables (review these for background context):**'); + for (const d of files) { + const refInfo = d.type === 'file' ? ` — File: \`${d.reference}\` (use \`file_read\` to inspect)` : + d.reference ? ` — ref: \`${d.reference}\`` : ''; + lines.push(`- ${d.summary ?? '(no summary)'}${refInfo}`); + } } } depSections.push(lines.join('\n')); @@ -1622,6 +1627,7 @@ export class TaskService { blockedBy, result: (row.result as Task['result']) ?? undefined, deliverables: Array.isArray((row as any).deliverables) ? ((row as any).deliverables as Task['deliverables']) : undefined, + completionSummary: (row as any).completionSummary ?? undefined, notes: Array.isArray(row.notes) ? (row.notes as string[]) : undefined, createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt), @@ -1649,6 +1655,36 @@ export class TaskService { } } + /** + * One-time migration: extract branch deliverable summaries into task.completionSummary + * and remove branch items from task.deliverables JSON. + */ + async migrateBranchToCompletionSummary(): Promise { + let migrated = 0; + for (const [taskId, task] of this.tasks) { + if (task.completionSummary) continue; + if (!task.deliverables?.length) continue; + + const branchItem = task.deliverables.find(d => d.type === 'branch'); + if (!branchItem) continue; + + task.completionSummary = branchItem.summary; + task.deliverables = task.deliverables.filter(d => d.type !== 'branch'); + task.updatedAt = new Date().toISOString(); + + if (this.taskRepo) { + this.taskRepo.updateCompletionSummary(taskId, task.completionSummary) + .catch(err => log.warn('Failed to persist completionSummary migration', { taskId, error: String(err) })); + this.taskRepo.updateDeliverables(taskId, task.deliverables) + .catch(err => log.warn('Failed to persist deliverables cleanup', { taskId, error: String(err) })); + } + migrated++; + } + if (migrated > 0) { + log.info(`Migrated branch->completionSummary for ${migrated} tasks`); + } + } + private static readonly PRIORITY_ORDER: Record = { urgent: 0, high: 1, medium: 2, low: 3, }; @@ -2968,7 +3004,7 @@ export class TaskService { // ─── Governance: Submit for Review ───────────────────────────────────────── - async submitForReview(taskId: string, deliverables: TaskDeliverable[], reviewerId?: string): Promise { + async submitForReview(taskId: string, deliverables: TaskDeliverable[], reviewerId?: string, completionSummary?: string): Promise { const task = this.tasks.get(taskId); if (!task) throw new Error(`Task not found: ${taskId}`); if (task.status !== 'in_progress') { @@ -3022,17 +3058,26 @@ export class TaskService { task.reviewerId = reviewerId; } - // Persist deliverables (and reviewerId if changed) to DB + // Persist completionSummary to the task + if (completionSummary) { + task.completionSummary = completionSummary; + } + + // Persist deliverables (and reviewerId/completionSummary if changed) to DB if (this.taskRepo) { this.taskRepo.updateDeliverables(task.id, task.deliverables) .catch(err => log.warn('Failed to persist deliverables to DB', { taskId: task.id, error: String(err) })); + if (completionSummary) { + this.taskRepo.updateCompletionSummary(task.id, completionSummary) + .catch(err => log.warn('Failed to persist completionSummary to DB', { taskId: task.id, error: String(err) })); + } if (reviewerId) { this.taskRepo.update(task.id, { reviewerId }) .catch(err => log.warn('Failed to persist reviewer change to DB', { taskId: task.id, error: String(err) })); } } - // Persist each task deliverable as a standalone Deliverable entity (skip branch — it's task metadata) + // Persist each task deliverable as a standalone Deliverable entity if (this.deliverableService && deliverables.length > 0) { const builderMode = this.detectBuilderMode(task.assignedAgentId); @@ -3143,8 +3188,13 @@ export class TaskService { parts.push(''); parts.push(`**Description:** ${task.description}`); + if (task.completionSummary) { + parts.push(''); + parts.push(`**Summary:** ${task.completionSummary}`); + } + if (task.deliverables && task.deliverables.length > 0) { - const files = task.deliverables.filter(d => d.type !== 'branch'); + const files = task.deliverables.filter(d => d.type !== 'branch' && d.reference); if (files.length > 0) { parts.push(''); parts.push('**Deliverables:**'); @@ -3153,11 +3203,6 @@ export class TaskService { } if (files.length > REVIEWER_FILE_LIST_MAX) parts.push(` ... and ${files.length - REVIEWER_FILE_LIST_MAX} more`); } - const branch = task.deliverables.find(d => d.type === 'branch'); - if (branch) { - parts.push(`**Branch:** ${branch.reference}`); - if (branch.summary) parts.push(`**Summary:** ${branch.summary}`); - } } // Include subtask status if any @@ -3866,13 +3911,18 @@ export class TaskService { lines.push('**Notes (most recent first):**'); for (const note of depTask.notes.slice(-PROMPT_DEP_NOTES_MAX).reverse()) lines.push(`- ${note.slice(0, PROMPT_DEP_NOTE_CHARS)}`); } + if (depTask.completionSummary) { + lines.push(`**Completion Summary:** ${depTask.completionSummary}`); + } if (depTask.deliverables?.length) { - lines.push('**Deliverables (review these for background context):**'); - for (const d of depTask.deliverables) { - const refInfo = d.type === 'file' ? ` — File: \`${d.reference}\` (use \`file_read\` to inspect)` : - d.type === 'branch' ? ` [branch: ${d.reference}]` : - d.reference ? ` — ref: \`${d.reference}\`` : ''; - lines.push(`- ${d.summary ?? '(no summary)'}${refInfo}`); + const files = depTask.deliverables.filter(d => d.type !== 'branch' && d.reference); + if (files.length > 0) { + lines.push('**Deliverables (review these for background context):**'); + for (const d of files) { + const refInfo = d.type === 'file' ? ` — File: \`${d.reference}\` (use \`file_read\` to inspect)` : + d.reference ? ` — ref: \`${d.reference}\`` : ''; + lines.push(`- ${d.summary ?? '(no summary)'}${refInfo}`); + } } } depSections.push(lines.join('\n')); diff --git a/packages/org-manager/test/deliverable-service.test.ts b/packages/org-manager/test/deliverable-service.test.ts index 3b4a57b1..f54fc266 100644 --- a/packages/org-manager/test/deliverable-service.test.ts +++ b/packages/org-manager/test/deliverable-service.test.ts @@ -11,6 +11,7 @@ function createMockRepo() { }), listAll: vi.fn(async () => [...rows.values()]), recordAccess: vi.fn().mockResolvedValue(undefined), + delete: vi.fn(async (id: string) => { rows.delete(id); }), listTaskIdsWithDeliverables: vi.fn(async () => new Set()), _rows: rows, }; @@ -187,17 +188,6 @@ describe('DeliverableService', () => { expect(service.get('dlv-db')?.title).toBe('Loaded'); }); - it('migrates deliverables from tasks', async () => { - const count = await service.migrateFromTasks([{ - id: 'task-1', - projectId: 'proj-1', - assignedAgentId: 'agent-1', - deliverables: [{ type: 'file', reference: '/out.txt', summary: 'output' }], - } as never]); - expect(count).toBe(1); - expect(service.findByTask('task-1')).toHaveLength(1); - }); - it('finds deliverables by agent and checks file health', async () => { const created = await service.create({ type: 'file', @@ -210,13 +200,5 @@ describe('DeliverableService', () => { expect(service.checkFileHealth('agent-1')).toContain(created.id); }); - it('skips re-migrating tasks that already have deliverables', async () => { - repo.listTaskIdsWithDeliverables.mockResolvedValue(new Set(['task-1'])); - const count = await service.migrateFromTasks([{ - id: 'task-1', - deliverables: [{ type: 'file', reference: '/out.txt', summary: 'output' }], - } as never]); - expect(count).toBe(0); - }); }); }); diff --git a/packages/shared/src/types/governance.ts b/packages/shared/src/types/governance.ts index 695db776..e47ab64a 100644 --- a/packages/shared/src/types/governance.ts +++ b/packages/shared/src/types/governance.ts @@ -46,6 +46,7 @@ export interface SystemAnnouncement { // ─── Task Delivery ─────────────────────────────────────────────────────────── export interface TaskDeliverable { + /** @deprecated 'branch' type is no longer produced — kept temporarily for backward compat during migration */ type: 'branch' | DeliverableType; reference: string; summary: string; diff --git a/packages/shared/src/types/task.ts b/packages/shared/src/types/task.ts index 9f03e87d..179593d3 100644 --- a/packages/shared/src/types/task.ts +++ b/packages/shared/src/types/task.ts @@ -122,6 +122,8 @@ export interface Task { /** Whether the reviewer is an agent or a human user (default: 'agent') */ reviewerType?: 'agent' | 'human'; deliverables?: TaskDeliverable[]; + /** Agent's completion summary (set on task_submit_review) */ + completionSummary?: string; // ── Scheduling fields ── /** 'standard' (default) or 'scheduled' for cron/recurring tasks */ diff --git a/packages/storage/src/sqlite-storage.ts b/packages/storage/src/sqlite-storage.ts index 2339ed04..08a1a670 100644 --- a/packages/storage/src/sqlite-storage.ts +++ b/packages/storage/src/sqlite-storage.ts @@ -706,6 +706,7 @@ export function openSqlite(dbPath: string): DatabaseSync { { table: 'deliverables', column: 'format', sql: 'ALTER TABLE deliverables ADD COLUMN format TEXT' }, { table: 'task_comments', column: 'reply_to_id', sql: 'ALTER TABLE task_comments ADD COLUMN reply_to_id TEXT' }, { table: 'requirement_comments', column: 'reply_to_id', sql: 'ALTER TABLE requirement_comments ADD COLUMN reply_to_id TEXT' }, + { table: 'tasks', column: 'completion_summary', sql: 'ALTER TABLE tasks ADD COLUMN completion_summary TEXT' }, ]; for (const m of migrations) { const cols = _db.prepare(`PRAGMA table_info(${m.table})`).all() as Array<{ name: string }>; @@ -1163,6 +1164,12 @@ export class SqliteTaskRepo { .run(toJson(deliverables), now(), id); } + async updateCompletionSummary(id: string, summary: string) { + this.db + .prepare('UPDATE tasks SET completion_summary = ?, updated_at = ? WHERE id = ?') + .run(summary, now(), id); + } + listByOrg(orgId: string, filters?: { status?: string; assignedAgentId?: string; projectId?: string; taskType?: string }) { let q = 'SELECT * FROM tasks WHERE org_id = ?'; const vals: SqlParams = [orgId]; @@ -1291,6 +1298,7 @@ export class SqliteTaskRepo { completedAt: toDate(r['completed_at'] as string), taskType: (r['task_type'] as string) ?? 'standard', scheduleConfig: fromJson(r['schedule_config'] as string), + completionSummary: (r['completion_summary'] as string) ?? undefined, createdAt: toDate(r['created_at'] as string), updatedAt: toDate(r['updated_at'] as string), dueAt: toDate(r['due_at'] as string), @@ -3525,6 +3533,10 @@ export class SqliteDeliverableRepo { this.db.prepare("UPDATE deliverables SET status = 'outdated', updated_at = ? WHERE id = ?").run(now(), id); } + async delete(id: string) { + this.db.prepare('DELETE FROM deliverables WHERE id = ?').run(id); + } + async listAll(limit = 500) { const rows = this.db.prepare("SELECT * FROM deliverables WHERE status != 'outdated' ORDER BY updated_at DESC LIMIT ?").all(limit) as Record[]; return rows.map(r => this.mapRow(r)); diff --git a/packages/storage/src/types.ts b/packages/storage/src/types.ts index a5075aa4..22406e32 100644 --- a/packages/storage/src/types.ts +++ b/packages/storage/src/types.ts @@ -332,6 +332,7 @@ export interface TaskRepo { assign(id: string, agentId: string, updatedBy?: string): Promise; update(id: string, data: Record): Promise; updateDeliverables(id: string, deliverables: unknown): Promise; + updateCompletionSummary(id: string, summary: string): Promise; } /** Contract for task log persistence */ @@ -358,6 +359,7 @@ export interface DeliverableRepo { create(data: Record): Promise; update(id: string, data: Record): Promise; recordAccess(id: string): Promise; + delete(id: string): Promise; listAll(limit?: number): Promise; listTaskIdsWithDeliverables(): Promise>; } diff --git a/packages/web-ui/src/pages/Deliverables.tsx b/packages/web-ui/src/pages/Deliverables.tsx index 4f5613fd..9c54fe59 100644 --- a/packages/web-ui/src/pages/Deliverables.tsx +++ b/packages/web-ui/src/pages/Deliverables.tsx @@ -765,8 +765,8 @@ export function DeliverablesPage({ authUser: _authUser, previewMode, previewData {/* Metadata */}
- {t('metadata.created')} {new Date(selected.createdAt).toLocaleDateString()} - {t('metadata.updated')} {new Date(selected.updatedAt).toLocaleDateString()} + {t('metadata.created')} {new Date(selected.createdAt).toLocaleString()} + {t('metadata.updated')} {new Date(selected.updatedAt).toLocaleString()} {selected.id.slice(0, 12)}
diff --git a/packages/web-ui/src/pages/Work.tsx b/packages/web-ui/src/pages/Work.tsx index 230612bd..5906c686 100644 --- a/packages/web-ui/src/pages/Work.tsx +++ b/packages/web-ui/src/pages/Work.tsx @@ -1708,6 +1708,14 @@ function TaskDetailPanel({ const [showRevision, setShowRevision] = useState(false); const [revisionReason, setRevisionReason] = useState(''); const [deliverablesPage, setDeliverablesPage] = useState(1); + const [unifiedDeliverables, setUnifiedDeliverables] = useState>([]); + const loadUnifiedDeliverables = useCallback(async () => { + try { + const { results } = await api.deliverables.search({ taskId: task.id, limit: 200 }); + setUnifiedDeliverables(results.filter((d: any) => d.status !== 'outdated')); + } catch { /* ok */ } + }, [task.id]); + useEffect(() => { void loadUnifiedDeliverables(); }, [loadUnifiedDeliverables]); const [descExpanded, setDescExpanded] = useState(false); const isMobile = useIsMobile(); const PAGE_SIZE = 20; @@ -1858,7 +1866,7 @@ function TaskDetailPanel({ {isRunning && }