Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@proma/electron",
"version": "0.14.6",
"version": "0.14.7",
"description": "Proma next gen ai software with general agents - Electron App",
"main": "dist/main.cjs",
"author": {
Expand Down
17 changes: 17 additions & 0 deletions apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import type {
GetTaskOutputResult,
StopTaskInput,
WorkspaceMcpConfig,
WorkspaceBoard,
SkillMeta,
SkillFileContent,
WorkspaceCapabilities,
Expand Down Expand Up @@ -225,6 +226,8 @@ import {
listWorkspaceAutoMemoryFiles,
readWorkspaceAutoMemoryFile,
writeWorkspaceAutoMemoryFile,
readWorkspaceBoard,
writeWorkspaceBoard,
getWorkspaceAttachedDirectories,
getWorkspaceAttachedFiles,
attachWorkspaceDirectory,
Expand Down Expand Up @@ -2203,6 +2206,20 @@ export function registerIpcHandlers(): void {
}
)

ipcMain.handle(
AGENT_IPC_CHANNELS.READ_WORKSPACE_BOARD,
async (_, workspaceSlug: string): Promise<WorkspaceBoard> => {
return readWorkspaceBoard(workspaceSlug)
}
)

ipcMain.handle(
AGENT_IPC_CHANNELS.WRITE_WORKSPACE_BOARD,
async (_, workspaceSlug: string, board: WorkspaceBoard): Promise<WorkspaceBoard> => {
return writeWorkspaceBoard(workspaceSlug, board)
}
)

// 发送 Agent 消息(触发 Agent SDK 流式响应)
ipcMain.handle(
AGENT_IPC_CHANNELS.SEND_MESSAGE,
Expand Down
2 changes: 2 additions & 0 deletions apps/electron/src/main/lib/agent-prompt-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ Proma 提供内置 \`collaboration\` 工具,可以创建真实可见的协作
存在两个 \`.context/\` 目录,用途不同:
- **会话级** \`.context/\`(当前 cwd 下):当前会话的临时工作台,存放本次任务的 todo.md、plan/、临时笔记等
- **工作区级** \`~/${configDirName}/agent-workspaces/${ctx.workspaceSlug}/workspace-files/.context/\`:跨会话共享的持久文档,存放长期 note.md、项目级知识等
- **工作区协作台** \`~/${configDirName}/agent-workspaces/${ctx.workspaceSlug}/workspace-files/.context/workspace-board.json\`:跨会话共享的结构化状态,记录当前目标、Todo、阻塞、决策草案、Proma 建议、自动任务/Skill 引用和待沉淀候选

选择写入哪个目录时:
- 只与当前任务相关的内容 → 会话级 \`.context/\`
Expand Down Expand Up @@ -263,6 +264,7 @@ Context 用来承载正在进行的任务状态、长期工作区资料和可搜

- **会话级 Context**:当前 cwd 下的 \`.context/\`,服务于本次任务,存放临时 todo、plan、研究笔记、handoff 和中间产物。任务结束后通常不需要长期维护。
- **工作区级 Context**:工作区 \`workspace-files/.context/\` 及其他工作区本地文档,跨会话共享,存放长期 note、调研、架构分析、决策记录、索引和大型证据材料。
- **工作区协作台**:\`workspace-files/.context/workspace-board.json\` 记录当前工作区仍在推进的目标、Todo、阻塞、待确认事项和 Proma 建议。新会话开始时应优先检查;任务推进后可以小幅更新,但不要把它当长期记忆或 Skill。它是结构化 JSON,顶层 schema 固定为 \`schemaVersion/title/summary/automationLevel/updatedAt/goals/todos/blockers/decisions/recommendations/automationRefs/skillRefs/knowledgeCandidates/notes\`;可在条目中使用 \`sourceRefs\` 关联 session、file、skill、automation、memory 或 board。主动维护规则由 \`automationLevel\` 决定:\`manual\` 只读取不主动新增条目;\`suggest\` 可写建议和候选项;\`assistive\` 可维护 Todo、阻塞和建议。任务结束后若仍有明确下一步,写入 1-3 条 \`todos\`;遇到需要用户决策或外部条件时写入 \`blockers\`;发现重复流程写 \`recommendations.kind=create_automation\`;发现可复用 SOP 写 \`recommendations.kind=create_skill\`;发现稳定经验但未经用户确认时写 \`knowledgeCandidates\` 或 \`recommendations.kind=promote_memory\`,不要直接改 Memory。
- **选择原则**:只对当前任务有用 → 会话级;未来多个会话会引用 → 工作区级;稳定规则摘要 → CLAUDE.md;经验/偏好/纠错 → Memory;重复流程 → Skill。

### .context/ 文档类型
Expand Down
104 changes: 103 additions & 1 deletion apps/electron/src/main/lib/agent-workspace-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, expect, test } from 'bun:test'
import { normalizeWorkspaceMcpConfig } from './agent-workspace-manager'
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
normalizeWorkspaceMcpConfig,
readWorkspaceBoard,
writeWorkspaceBoard,
} from './agent-workspace-manager'
import { getAgentWorkspacePath } from './config-paths'

describe('Agent 工作区 MCP 配置', () => {
test('Given 工作区 MCP 包含内置保留名 When 归一化配置 Then 剔除冲突项并保留普通服务器', () => {
Expand All @@ -26,4 +33,99 @@ describe('Agent 工作区 MCP 配置', () => {
expect(Object.keys(normalized.servers).sort()).toEqual(['github'])
expect(normalized.servers.github?.command).toBe('github-mcp')
})

test('Given 工作区已有旧 Markdown 协作台 When 读取协作台 Then 转为结构化 JSON schema', () => {
const workspaceSlug = `legacy-board-workspace-${Date.now()}-${Math.random().toString(36).slice(2)}`

try {
const contextDir = join(getAgentWorkspacePath(workspaceSlug), 'workspace-files', '.context')
mkdirSync(contextDir, { recursive: true })
writeFileSync(join(contextDir, 'workspace-board.md'), '# 协作台\n\n- [ ] 迁移旧 Todo\n', 'utf-8')

const board = readWorkspaceBoard(workspaceSlug)
expect(board.schemaVersion).toBe(1)
expect(board.todos[0]?.title).toBe('迁移旧 Todo')
expect(board.recommendations[0]?.kind).toBe('follow_up')
expect(board.recommendations[0]?.sourceRefs?.[0]?.path).toBe('.context/workspace-board.md')
expect(board.notes[0]?.source).toBe('.context/workspace-board.md')
} finally {
rmSync(getAgentWorkspacePath(workspaceSlug), { recursive: true, force: true })
}
})
})

describe('Agent 工作区协作台', () => {
test('Given 工作区没有协作台文件 When 保存协作台内容 Then 写入 workspace-files/.context 并可再次读取', () => {
const workspaceSlug = `board-workspace-${Date.now()}-${Math.random().toString(36).slice(2)}`

try {
const empty = readWorkspaceBoard(workspaceSlug)
expect(empty.schemaVersion).toBe(1)
expect(empty.todos).toEqual([])
expect(empty.recommendations).toEqual([])
expect(empty.automationRefs).toEqual([])
expect(empty.skillRefs).toEqual([])
expect(empty.automationLevel).toBe('suggest')

writeWorkspaceBoard(workspaceSlug, {
...empty,
automationLevel: 'assistive',
recommendations: [{
id: 'recommendation-1',
title: '创建自动任务跟进阻塞',
kind: 'create_automation',
status: 'suggested',
confidence: 1.2,
safetyLevel: 'creates_automation',
sourceRefs: [{ type: 'board', id: 'blocker-1', title: '阻塞记录' }],
createdAt: '2026-07-05T00:00:00.000Z',
updatedAt: '2026-07-05T00:00:00.000Z',
}],
automationRefs: [{
id: 'automation-ref-1',
title: '每日状态扫描',
status: 'linked',
automationId: 'automation-1',
trigger: 'daily',
createdAt: '2026-07-05T00:00:00.000Z',
updatedAt: '2026-07-05T00:00:00.000Z',
}],
skillRefs: [{
id: 'skill-ref-1',
title: '缺陷扫描流程',
status: 'enabled',
skillSlug: 'proma-daily-defect-scan',
createdAt: '2026-07-05T00:00:00.000Z',
updatedAt: '2026-07-05T00:00:00.000Z',
}],
todos: [{
id: 'todo-1',
title: '跟进状态',
status: 'in_progress',
owner: 'agent',
createdAt: '2026-07-05T00:00:00.000Z',
updatedAt: '2026-07-05T00:00:00.000Z',
}],
})

const saved = readWorkspaceBoard(workspaceSlug)
expect(saved.schemaVersion).toBe(1)
expect(saved.automationLevel).toBe('assistive')
expect(saved.todos[0]?.title).toBe('跟进状态')
expect(saved.todos[0]?.status).toBe('in_progress')
expect(saved.recommendations[0]?.confidence).toBe(1)
expect(saved.recommendations[0]?.sourceRefs?.[0]?.type).toBe('board')
expect(saved.automationRefs[0]?.trigger).toBe('daily')
expect(saved.skillRefs[0]?.skillSlug).toBe('proma-daily-defect-scan')

expect(existsSync(join(
getAgentWorkspacePath(workspaceSlug),
'workspace-files',
'.context',
'workspace-board.json',
))).toBe(true)
} finally {
rmSync(getAgentWorkspacePath(workspaceSlug), { recursive: true, force: true })
}
})
})
Loading