diff --git a/apps/desktop/README.md b/apps/desktop/README.md
index 4654ea1b..01cbfa36 100644
--- a/apps/desktop/README.md
+++ b/apps/desktop/README.md
@@ -466,255 +466,26 @@ pnpm --filter @multica/desktop add -D @types/qrcode.react
---
-## 八、实现步骤计划
+## 八、待办事项
-### Phase 1: 统一布局与路由重构
+### 功能开发
-**目标**: 统一页面结构,移除 /admin 子路由
+#### 设备授权与白名单
-#### Step 1.1: 路由重构
+- [ ] Client 连接授权弹窗
+- [ ] 已授权设备白名单管理
+- [ ] 显示当前连接的 Client 数量
-- [x] 重构 `App.tsx` 路由
- - 移除 `/admin` 子路由
- - 统一页面结构: / (Home) / /chat / /tools / /skills
-- [x] 创建 `pages/layout.tsx` - 全局布局
- - Header: Logo + 标题 + Settings 按钮
- - Tabs: Home / Chat / Tools / Skills
- - Content Area: 子路由出口
-- [x] 移动页面文件到根级别
+#### Settings 页面
-#### Step 1.2: Home 页面 (三入口)
+- [ ] Gateway URL 配置
+- [ ] Theme 切换 (Light / Dark / System)
+- [ ] 打开 credentials.json5 按钮
-- [x] 重构 `pages/home.tsx`
- - 左侧二维码 + 右侧 Agent 状态面板
- - 底部: Open Chat 按钮 + Connect to Remote (Coming soon)
-- [x] 安装 `qrcode.react` 依赖
-- [x] 创建 `components/qr-code.tsx` - 分享二维码组件
- - 生成二维码数据 (hubId, agentId, token, gateway, expires)
- - 倒计时显示 + 自动过期刷新
- - Refresh 按钮 + Copy Link 按钮
- - 装饰性角落边框
+### 联调测试
-#### Step 1.3: Chat 页面 (双模式)
-
-- [ ] 重构 `pages/chat.tsx`
- - 顶部模式切换: Local Agent / Remote Agent
- - 支持本地 Agent 直接调用
- - 支持远程 Agent WebSocket 连接
-- [ ] 创建 `hooks/use-local-agent.ts` - 本地 Agent 调用
-- [ ] 创建 `hooks/use-remote-agent.ts` - 远程 Agent 连接
-
-**交付物**: 统一的页面结构,Home 页面三入口可用
-
----
-
-### Phase 2: IPC 集成与 Hub 启动 ✅ (完成)
-
-**目标**: 在 Main Process 中启动 Hub,通过 IPC 与 Renderer 通信
-
-#### Step 2.1: IPC 基础设施
-
-- [x] 创建 `electron/ipc/` 目录结构
-- [x] 创建 `electron/ipc/index.ts` - 统一注册 handlers
-- [x] 创建 `electron/ipc/agent.ts` - Tools 相关 IPC handlers
-- [x] 创建 `electron/ipc/skills.ts` - Skills 相关 IPC handlers
-- [x] 更新 `electron/main.ts` - 注册 IPC handlers
-
-#### Step 2.2: Hub 集成
-
-- [x] 创建 `electron/ipc/hub.ts` - Hub 管理
-- [x] 实现 Hub 自动启动 (App ready 时)
-- [x] 实现 Agent 自动创建
-- [x] 实现 Hub 状态查询 (`hub:getStatus`)
-
-#### Step 2.3: Preload 脚本
-
-- [x] 更新 `electron/preload.ts`
- - 暴露 `window.electronAPI.hub.*`
- - 暴露 `window.electronAPI.tools.*`
- - 暴露 `window.electronAPI.skills.*`
-
-#### Step 2.4: Hooks 更新
-
-- [x] 更新 `hooks/use-tools.ts` - 调用 IPC
-- [x] 更新 `hooks/use-skills.ts` - 调用 IPC
-- [x] 创建 `hooks/use-hub.ts` - Hub 状态
-
-**交付物**: Hub 在主进程运行,UI 可通过 IPC 获取真实数据
-
----
-
-### Phase 3: Tools 管理页面
-
-**目标**: 查看和管理 Agent Tools
-
-#### Step 3.1: Tools 数据获取
-
-- [x] 创建 `hooks/use-tools.ts`
- - 获取所有 tools 列表
- - 获取 tool groups 和 profiles
- - 管理 allow/deny 状态
-
-#### Step 3.2: Tools UI 组件
-
-- [x] 创建 `components/tool-list.tsx`
- - 表格展示: Name / Group / Status / Toggle
- - 按 Group 分组折叠
- - 开关切换 (Switch 组件)
- - Profile 下拉选择器 (内置)
- - Reset to Default 按钮 (内置)
-
-#### Step 3.3: Tools 页面整合
-
-- [x] 更新 `pages/tools.tsx`
- - Profile 选择器
- - Tool 列表
- - (状态持久化待后续实现)
-
-#### Step 3.4: Tools 实时同步
-
-- [x] 实现 `tools:list` 从真实 Agent 获取活跃 tools
-- [x] 实现 `tools:active` 获取当前活跃工具
-- [x] 实现 `tools:reload` 调用 Agent.reloadTools()
-- [x] 暴露 AsyncAgent.getActiveTools() 和 reloadTools() 方法
-- [x] 实现 `tools:setStatus` 持久化到 profile config.json
-- [ ] 验证 Tool 开关影响 Agent 行为
-
-**交付物**: 可查看所有 Tools,切换 Profile,开关单个 Tool,实时影响 Agent
-
----
-
-### Phase 4: Skills 管理页面
-
-**目标**: 查看、添加、删除 Skills
-
-#### Step 4.1: Skills 数据获取
-
-- [x] 创建 `hooks/use-skills.ts`
- - 加载所有 skills (mock data for now)
- - 检查 eligibility
- - 添加/删除/安装操作 (stub)
-
-#### Step 4.2: Skills UI 组件
-
-- [x] 创建 `components/skill-list.tsx`
- - 表格展示: Name / Source / Status / Actions
- - Status 徽章 (ready / missing / disabled)
- - Action 按钮 (View / Install / Delete)
- - Add Skill dialog (内置 skills.tsx)
- - View Skill dialog (内置 skills.tsx)
-
-#### Step 4.3: Skills 页面整合
-
-- [x] 更新 `pages/skills.tsx`
- - Skill 列表
- - Add Skill 按钮 + dialog
- - View Skill dialog
- - Refresh 按钮
-
-#### Step 4.4: Skills IPC 集成
-
-- [x] 在 Agent 中添加 `getSkillsWithStatus()` 方法
-- [x] 在 AsyncAgent 中暴露 `getSkillsWithStatus()` 方法
-- [x] 实现 `skills:list` 从真实 Agent 获取 skills
-- [x] 实现 `skills:get` 获取单个 skill 详情
-- [x] 实现 `skills:toggle` 返回当前 eligibility 状态
-- [x] 实现 `skills:reload` 重新加载 skills
-- [x] 实现 `skills:add` 调用 `addSkill()`
-- [x] 实现 `skills:remove` 调用 `removeSkill()`
-
-**交付物**: 可查看所有 Skills,查看 Skill 详情,显示 eligibility 状态
-
----
-
-### Phase 5: 设置与完善
-
-**目标**: Settings 页面 + 体验优化
-
-#### Step 5.1: Settings 页面
-
-- [ ] 创建 `components/settings-dialog.tsx`
- - Gateway URL 配置
- - Theme 切换 (Light / Dark / System)
- - 打开 credentials.json5 按钮
-
-#### Step 5.2: 连接状态管理
-
-- [ ] 创建 `components/connection-status.tsx`
- - 显示 Gateway 连接状态
- - 显示已连接的 Client 信息
- - 显示 Agent 状态
-
-#### Step 5.3: 体验优化
-
-- [ ] Toast 通知 (操作成功/失败)
-- [ ] Loading 状态优化 (各页面)
-- [ ] 错误边界处理 (React Error Boundary)
-- [ ] 二维码自动刷新 (5 分钟过期后自动刷新)
-
-**交付物**: 完整的管理功能,良好的用户体验
-
----
-
-### Phase 6: Chat 页面与 Agent 联调
-
-**目标**: 实现 Chat 功能,支持本地和远程 Agent
-
-#### Step 6.1: 本地 Chat 实现
-
-- [ ] 重构 `pages/chat.tsx`
- - 消息输入框 + 发送按钮
- - 消息历史展示
- - 流式响应显示
-- [ ] 创建 `hooks/use-local-agent.ts`
- - 通过 IPC 调用 Agent.run()
- - 处理流式响应
- - 管理消息历史
-
-#### Step 6.2: 远程 Chat 实现
-
-- [ ] 创建 `hooks/use-remote-agent.ts`
- - 通过 Gateway WebSocket 连接
- - 处理远程消息
-- [ ] Chat 页面模式切换
- - Local Mode / Remote Mode 切换
-
-**交付物**: 可与本地 Agent 对话,可连接远程 Agent
-
----
-
-### Phase 7: 联调与测试
-
-**目标**: 完整流程联调
-
-#### Step 7.1: 本地 Agent 联调
-
-- [ ] Tools 开关实时影响 Agent
+- [ ] Tools 开关实时影响 Agent 行为
- [ ] Skills 启用/禁用影响 Agent
-- [ ] Chat 流式响应正常
-
-#### Step 7.2: 远程连接联调
-
- [ ] 扫码连接远程 Agent
- [ ] Token 验证流程
-- [ ] 消息流转测试
-
-#### Step 7.3: 异常处理
-
-- [ ] 断开重连
-- [ ] Token 过期处理
-- [ ] Gateway 断开处理
-
----
-
-## 九、当前进度摘要
-
-| Phase | 名称 | 状态 |
-| ------- | -------------- | ----------------------- |
-| Phase 1 | 布局与路由 | ✅ 完成 |
-| Phase 2 | IPC 集成与 Hub | ✅ 完成 |
-| Phase 3 | Tools 管理 | ✅ UI + IPC 集成完成 |
-| Phase 4 | Skills 管理 | ✅ UI + IPC 集成完成 |
-| Phase 5 | 设置与完善 | ⏳ 待开始 |
-| Phase 6 | Chat 页面 | ⏳ 待开始 (同事负责 UI) |
-| Phase 7 | 联调测试 | ⏳ 待开始 |
+- [ ] 断开重连、Token 过期处理
diff --git a/apps/desktop/electron/electron-env.d.ts b/apps/desktop/electron/electron-env.d.ts
index 5f644318..4426985c 100644
--- a/apps/desktop/electron/electron-env.d.ts
+++ b/apps/desktop/electron/electron-env.d.ts
@@ -78,6 +78,12 @@ interface SkillAddResult {
skills?: string[]
}
+interface ProfileData {
+ profileId: string | undefined
+ name: string | undefined
+ userContent: string | undefined
+}
+
interface ElectronAPI {
hub: {
init: () => Promise
@@ -116,6 +122,11 @@ interface ElectronAPI {
agent: {
status: () => Promise
}
+ profile: {
+ get: () => Promise
+ updateName: (name: string) => Promise
+ updateUser: (content: string) => Promise
+ }
}
// Used in Renderer process, expose in `preload.ts`
diff --git a/apps/desktop/electron/ipc/agent.ts b/apps/desktop/electron/ipc/agent.ts
index 73ca65b5..a358b88a 100644
--- a/apps/desktop/electron/ipc/agent.ts
+++ b/apps/desktop/electron/ipc/agent.ts
@@ -125,8 +125,6 @@ export function registerAgentIpcHandlers(): void {
* Persists the change to profile config and reloads tools.
*/
ipcMain.handle('tools:toggle', async (_event, toolName: string) => {
- console.log(`[IPC] tools:toggle called for: ${toolName}`)
-
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
@@ -145,7 +143,6 @@ export function registerAgentIpcHandlers(): void {
// Get updated status
const newActiveTools = agent.getActiveTools()
const isNowEnabled = newActiveTools.includes(toolName)
- console.log(`[IPC] Tool ${toolName} toggled: ${isCurrentlyEnabled} -> ${isNowEnabled}`)
return {
name: toolName,
diff --git a/apps/desktop/electron/ipc/hub.ts b/apps/desktop/electron/ipc/hub.ts
index 910e8a50..24208586 100644
--- a/apps/desktop/electron/ipc/hub.ts
+++ b/apps/desktop/electron/ipc/hub.ts
@@ -13,31 +13,43 @@ import type { AsyncAgent } from '../../../../src/agent/async-agent.js'
let hub: Hub | null = null
let defaultAgentId: string | null = null
+/**
+ * Safe log function that catches EPIPE errors.
+ * Electron main process stdout can be closed unexpectedly.
+ */
+function safeLog(...args: unknown[]): void {
+ try {
+ console.log(...args)
+ } catch {
+ // Ignore EPIPE errors when stdout is closed
+ }
+}
+
/**
* Initialize Hub on app startup.
* Creates Hub and a default Agent automatically.
*/
export async function initializeHub(): Promise {
if (hub) {
- console.log('[Desktop] Hub already initialized')
+ safeLog('[Desktop] Hub already initialized')
return
}
const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000'
- console.log(`[Desktop] Initializing Hub, connecting to Gateway: ${gatewayUrl}`)
+ safeLog(`[Desktop] Initializing Hub, connecting to Gateway: ${gatewayUrl}`)
hub = new Hub(gatewayUrl)
// Create default agent if none exists
const agents = hub.listAgents()
if (agents.length === 0) {
- console.log('[Desktop] Creating default agent...')
+ safeLog('[Desktop] Creating default agent...')
const agent = hub.createAgent()
defaultAgentId = agent.sessionId
- console.log(`[Desktop] Default agent created: ${defaultAgentId}`)
+ safeLog(`[Desktop] Default agent created: ${defaultAgentId}`)
} else {
defaultAgentId = agents[0]
- console.log(`[Desktop] Using existing agent: ${defaultAgentId}`)
+ safeLog(`[Desktop] Using existing agent: ${defaultAgentId}`)
}
}
@@ -47,7 +59,7 @@ export async function initializeHub(): Promise {
function getHub(): Hub {
if (!hub) {
const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000'
- console.log(`[Desktop] Creating Hub, connecting to Gateway: ${gatewayUrl}`)
+ safeLog(`[Desktop] Creating Hub, connecting to Gateway: ${gatewayUrl}`)
hub = new Hub(gatewayUrl)
}
return hub
@@ -289,7 +301,7 @@ export function setupDeviceConfirmation(mainWindow: Electron.BrowserWindow): voi
*/
export function cleanupHub(): void {
if (hub) {
- console.log('[Desktop] Shutting down Hub')
+ safeLog('[Desktop] Shutting down Hub')
hub.shutdown()
hub = null
}
diff --git a/apps/desktop/electron/ipc/index.ts b/apps/desktop/electron/ipc/index.ts
index 13ad6fe4..e1ac16d2 100644
--- a/apps/desktop/electron/ipc/index.ts
+++ b/apps/desktop/electron/ipc/index.ts
@@ -4,10 +4,12 @@
export { registerAgentIpcHandlers, cleanupAgent } from './agent.js'
export { registerSkillsIpcHandlers } from './skills.js'
export { registerHubIpcHandlers, cleanupHub, initializeHub, setupDeviceConfirmation } from './hub.js'
+export { registerProfileIpcHandlers } from './profile.js'
import { registerAgentIpcHandlers, cleanupAgent } from './agent.js'
import { registerSkillsIpcHandlers } from './skills.js'
import { registerHubIpcHandlers, cleanupHub, initializeHub, setupDeviceConfirmation } from './hub.js'
+import { registerProfileIpcHandlers } from './profile.js'
/**
* Register all IPC handlers.
@@ -17,6 +19,7 @@ export function registerAllIpcHandlers(): void {
registerHubIpcHandlers()
registerAgentIpcHandlers()
registerSkillsIpcHandlers()
+ registerProfileIpcHandlers()
}
/**
diff --git a/apps/desktop/electron/ipc/profile.ts b/apps/desktop/electron/ipc/profile.ts
new file mode 100644
index 00000000..ed30130f
--- /dev/null
+++ b/apps/desktop/electron/ipc/profile.ts
@@ -0,0 +1,91 @@
+/**
+ * Profile IPC handlers for Electron main process.
+ *
+ * Manages agent profile settings like name and user.md content.
+ */
+import { ipcMain } from 'electron'
+import { getCurrentHub } from './hub.js'
+
+/**
+ * Get the default agent from Hub.
+ */
+function getDefaultAgent() {
+ const hub = getCurrentHub()
+ if (!hub) return null
+
+ const agentIds = hub.listAgents()
+ if (agentIds.length === 0) return null
+
+ return hub.getAgent(agentIds[0]) ?? null
+}
+
+/**
+ * Profile data returned to renderer.
+ */
+export interface ProfileData {
+ profileId: string | undefined
+ name: string | undefined
+ userContent: string | undefined
+}
+
+/**
+ * Register all Profile-related IPC handlers.
+ */
+export function registerProfileIpcHandlers(): void {
+ /**
+ * Get profile data (name + user content).
+ */
+ ipcMain.handle('profile:get', async (): Promise => {
+ const agent = getDefaultAgent()
+ if (!agent) {
+ return {
+ profileId: undefined,
+ name: undefined,
+ userContent: undefined,
+ }
+ }
+
+ return {
+ profileId: agent.getProfileId(),
+ name: agent.getAgentName(),
+ userContent: agent.getUserContent(),
+ }
+ })
+
+ /**
+ * Update agent display name.
+ */
+ ipcMain.handle('profile:updateName', async (_event, name: string) => {
+ const agent = getDefaultAgent()
+ if (!agent) {
+ return { error: 'No agent available' }
+ }
+
+ agent.setAgentName(name)
+ return { ok: true, name }
+ })
+
+ /**
+ * Update user.md content.
+ */
+ ipcMain.handle('profile:updateUser', async (_event, content: string) => {
+ const agent = getDefaultAgent()
+ if (!agent) {
+ console.error('[Profile IPC] No agent available for updateUser')
+ return { error: 'No agent available' }
+ }
+
+ console.log('[Profile IPC] Updating user content:', content.substring(0, 50) + '...')
+ agent.setUserContent(content)
+
+ // Reload system prompt to apply changes immediately
+ console.log('[Profile IPC] Reloading system prompt...')
+ agent.reloadSystemPrompt()
+
+ // Verify the change
+ const newUserContent = agent.getUserContent()
+ console.log('[Profile IPC] New user content:', newUserContent?.substring(0, 50) + '...')
+
+ return { ok: true }
+ })
+}
diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
index 193ca84d..bdf7168c 100644
--- a/apps/desktop/electron/main.ts
+++ b/apps/desktop/electron/main.ts
@@ -1,3 +1,49 @@
+// Patch console methods to handle EPIPE errors in Electron main process
+// This MUST be done before any other imports that might use console
+// EPIPE happens when stdout/stderr pipes are closed unexpectedly
+const originalConsoleLog = console.log.bind(console)
+const originalConsoleError = console.error.bind(console)
+const originalConsoleWarn = console.warn.bind(console)
+
+const safeLog = (...args: unknown[]) => {
+ try {
+ originalConsoleLog(...args)
+ } catch {
+ // Ignore EPIPE errors silently
+ }
+}
+
+const safeError = (...args: unknown[]) => {
+ try {
+ originalConsoleError(...args)
+ } catch {
+ // Ignore EPIPE errors silently
+ }
+}
+
+const safeWarn = (...args: unknown[]) => {
+ try {
+ originalConsoleWarn(...args)
+ } catch {
+ // Ignore EPIPE errors silently
+ }
+}
+
+// Override global console
+console.log = safeLog
+console.error = safeError
+console.warn = safeWarn
+
+// Also handle process stdout/stderr EPIPE errors
+process.stdout?.on?.('error', (err: NodeJS.ErrnoException) => {
+ if (err.code === 'EPIPE') return // Ignore
+ throw err
+})
+process.stderr?.on?.('error', (err: NodeJS.ErrnoException) => {
+ if (err.code === 'EPIPE') return // Ignore
+ throw err
+})
+
import { app, BrowserWindow } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts
index 6eb74ebc..c9070f01 100644
--- a/apps/desktop/electron/preload.ts
+++ b/apps/desktop/electron/preload.ts
@@ -37,6 +37,12 @@ export interface SkillInfo {
triggers: string[]
}
+export interface ProfileData {
+ profileId: string | undefined
+ name: string | undefined
+ userContent: string | undefined
+}
+
// ============================================================================
// Expose typed API to Renderer process
// ============================================================================
@@ -97,6 +103,13 @@ const electronAPI = {
agent: {
status: () => ipcRenderer.invoke('agent:status'),
},
+
+ // Profile management
+ profile: {
+ get: (): Promise => ipcRenderer.invoke('profile:get'),
+ updateName: (name: string) => ipcRenderer.invoke('profile:updateName', name),
+ updateUser: (content: string) => ipcRenderer.invoke('profile:updateUser', content),
+ },
}
// Expose to renderer
diff --git a/apps/desktop/src/components/agent-settings-dialog.tsx b/apps/desktop/src/components/agent-settings-dialog.tsx
new file mode 100644
index 00000000..3f44d548
--- /dev/null
+++ b/apps/desktop/src/components/agent-settings-dialog.tsx
@@ -0,0 +1,130 @@
+import { useState, useEffect } from 'react'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+} from '@multica/ui/components/ui/dialog'
+import { Button } from '@multica/ui/components/ui/button'
+import { Input } from '@multica/ui/components/ui/input'
+import { Textarea } from '@multica/ui/components/ui/textarea'
+import { Label } from '@multica/ui/components/ui/label'
+import { HugeiconsIcon } from '@hugeicons/react'
+import { Loading03Icon } from '@hugeicons/core-free-icons'
+
+interface AgentSettingsDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogProps) {
+ const [loading, setLoading] = useState(false)
+ const [saving, setSaving] = useState(false)
+ const [name, setName] = useState('')
+ const [userContent, setUserContent] = useState('')
+ const [profileId, setProfileId] = useState()
+
+ // Load profile data when dialog opens
+ useEffect(() => {
+ if (open) {
+ loadProfile()
+ }
+ }, [open])
+
+ const loadProfile = async () => {
+ setLoading(true)
+ try {
+ const data = await window.electronAPI.profile.get()
+ setProfileId(data.profileId)
+ setName(data.name ?? '')
+ setUserContent(data.userContent ?? '')
+ } catch (err) {
+ console.error('Failed to load profile:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSave = async () => {
+ setSaving(true)
+ try {
+ // Update name if changed
+ await window.electronAPI.profile.updateName(name)
+ // Update user content
+ await window.electronAPI.profile.updateUser(userContent)
+ onOpenChange(false)
+ } catch (err) {
+ console.error('Failed to save profile:', err)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default AgentSettingsDialog
diff --git a/apps/desktop/src/pages/home.tsx b/apps/desktop/src/pages/home.tsx
index be7257f7..ba2767f0 100644
--- a/apps/desktop/src/pages/home.tsx
+++ b/apps/desktop/src/pages/home.tsx
@@ -1,3 +1,4 @@
+import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button } from '@multica/ui/components/ui/button'
import { HugeiconsIcon } from '@hugeicons/react'
@@ -6,14 +7,39 @@ import {
LinkSquare01Icon,
Loading03Icon,
AlertCircleIcon,
+ Edit02Icon,
} from '@hugeicons/core-free-icons'
import { ConnectionQRCode } from '../components/qr-code'
import { DeviceList } from '../components/device-list'
+import { AgentSettingsDialog } from '../components/agent-settings-dialog'
import { useHub } from '../hooks/use-hub'
export default function HomePage() {
const navigate = useNavigate()
const { hubInfo, agents, loading, error } = useHub()
+ const [settingsOpen, setSettingsOpen] = useState(false)
+ const [agentName, setAgentName] = useState()
+
+ // Load agent profile info
+ useEffect(() => {
+ loadAgentInfo()
+ }, [])
+
+ // Reload agent info when settings dialog closes
+ useEffect(() => {
+ if (!settingsOpen) {
+ loadAgentInfo()
+ }
+ }, [settingsOpen])
+
+ const loadAgentInfo = async () => {
+ try {
+ const data = await window.electronAPI.profile.get()
+ setAgentName(data.name)
+ } catch (err) {
+ console.error('Failed to load agent info:', err)
+ }
+ }
// Get the first agent (or create one if none exists)
const primaryAgent = agents[0]
@@ -108,6 +134,23 @@ export default function HomePage() {
+ {/* Agent Settings */}
+
+
+
+ Agent Settings
+
+
+
+
{agentName || 'Unnamed Agent'}
+
+
{/* Stats Grid */}
@@ -148,6 +191,9 @@ export default function HomePage() {
+ {/* Agent Settings Dialog */}
+
+
{/* Bottom: Actions */}
diff --git a/src/agent/async-agent.ts b/src/agent/async-agent.ts
index 86ae5d90..b95fdc50 100644
--- a/src/agent/async-agent.ts
+++ b/src/agent/async-agent.ts
@@ -148,4 +148,40 @@ export class AsyncAgent {
getProfileId(): string | undefined {
return this.agent.getProfileId();
}
+
+ /**
+ * Get agent display name from profile config.
+ */
+ getAgentName(): string | undefined {
+ return this.agent.getAgentName();
+ }
+
+ /**
+ * Update agent display name in profile config.
+ */
+ setAgentName(name: string): void {
+ this.agent.setAgentName(name);
+ }
+
+ /**
+ * Get user.md content from profile.
+ */
+ getUserContent(): string | undefined {
+ return this.agent.getUserContent();
+ }
+
+ /**
+ * Update user.md content in profile.
+ */
+ setUserContent(content: string): void {
+ this.agent.setUserContent(content);
+ }
+
+ /**
+ * Reload profile from disk and rebuild system prompt.
+ * Call this after updating profile files to apply changes immediately.
+ */
+ reloadSystemPrompt(): void {
+ this.agent.reloadSystemPrompt();
+ }
}
diff --git a/src/agent/profile/index.ts b/src/agent/profile/index.ts
index bc41e046..a8c6dd9d 100644
--- a/src/agent/profile/index.ts
+++ b/src/agent/profile/index.ts
@@ -13,7 +13,10 @@ import {
loadProfile,
profileExists,
saveProfile,
+ writeProfileConfig,
+ writeProfileFile,
} from "./storage.js";
+import { PROFILE_FILES } from "./types.js";
export { type AgentProfile, type CreateProfileOptions, type ProfileConfig, type ProfileManagerOptions } from "./types.js";
export { DEFAULT_TEMPLATES } from "./templates.js";
@@ -110,6 +113,12 @@ export class ProfileManager {
return this.profile;
}
+ /** 重新从磁盘加载 profile(清除缓存) */
+ reloadProfile(): AgentProfile | undefined {
+ this.profile = loadAgentProfile(this.profileId, { baseDir: this.baseDir });
+ return this.profile;
+ }
+
/** 获取或创建 profile */
getOrCreateProfile(useTemplates = true): AgentProfile {
if (!this.profile) {
@@ -129,6 +138,7 @@ export class ProfileManager {
/** 构建 system prompt */
buildSystemPrompt(): string {
const profile = this.getProfile();
+ console.log('[ProfileManager] buildSystemPrompt called, profile exists:', !!profile);
if (!profile) {
return "";
}
@@ -136,11 +146,15 @@ export class ProfileManager {
const parts: string[] = [];
if (profile.soul) {
+ console.log('[ProfileManager] Adding soul, length:', profile.soul.length);
parts.push(profile.soul);
}
if (profile.user) {
+ console.log('[ProfileManager] Adding user, content:', profile.user.substring(0, 100));
parts.push(profile.user);
+ } else {
+ console.log('[ProfileManager] No user content in profile');
}
if (profile.workspace) {
@@ -215,4 +229,72 @@ export class ProfileManager {
this.updateToolsConfig(newConfig);
return newConfig;
}
+
+ /** 获取 Agent 名称 */
+ getName(): string | undefined {
+ const profile = this.getProfile();
+ return profile?.config?.name;
+ }
+
+ /** 更新 Agent 名称 */
+ updateName(name: string): void {
+ const profile = this.getOrCreateProfile(false);
+ const currentConfig = profile.config ?? {};
+ const newConfig: ProfileConfig = {
+ ...currentConfig,
+ name,
+ };
+ profile.config = newConfig;
+ this.profile = profile;
+ writeProfileConfig(this.profileId, newConfig, { baseDir: this.baseDir });
+
+ // Also update soul.md to include the agent name
+ this.updateSoulWithName(name);
+ }
+
+ /** 更新 soul.md,确保包含 Agent 名称 */
+ private updateSoulWithName(name: string): void {
+ const profile = this.getOrCreateProfile(true); // 确保有默认模板
+ let soulContent = profile.soul ?? DEFAULT_TEMPLATES.soul;
+
+ // 替换 soul.md 中的 Name 字段
+ // 匹配 "- **Name:** xxx" 格式
+ const namePattern = /- \*\*Name:\*\* .*/;
+ const newNameLine = `- **Name:** ${name}`;
+
+ if (namePattern.test(soulContent)) {
+ soulContent = soulContent.replace(namePattern, newNameLine);
+ } else {
+ // 如果没有找到 Name 字段,在 Identity 部分后添加
+ const identityPattern = /## Identity\n/;
+ if (identityPattern.test(soulContent)) {
+ soulContent = soulContent.replace(identityPattern, `## Identity\n\n${newNameLine}\n`);
+ } else {
+ // 如果没有 Identity 部分,在开头添加
+ soulContent = `# Soul\n\n## Identity\n\n${newNameLine}\n\n${soulContent}`;
+ }
+ }
+
+ // 保存更新后的 soul.md
+ writeProfileFile(this.profileId, PROFILE_FILES.soul, soulContent, { baseDir: this.baseDir });
+ // 更新缓存
+ if (this.profile) {
+ this.profile.soul = soulContent;
+ }
+ }
+
+ /** 获取 user.md 内容 */
+ getUserContent(): string | undefined {
+ const profile = this.getProfile();
+ return profile?.user;
+ }
+
+ /** 更新 user.md 内容 */
+ updateUserContent(content: string): void {
+ writeProfileFile(this.profileId, PROFILE_FILES.user, content, { baseDir: this.baseDir });
+ // Update cached profile
+ if (this.profile) {
+ this.profile.user = content;
+ }
+ }
}
diff --git a/src/agent/profile/templates.ts b/src/agent/profile/templates.ts
index 08378667..28c2fec2 100644
--- a/src/agent/profile/templates.ts
+++ b/src/agent/profile/templates.ts
@@ -3,7 +3,7 @@
*/
export const DEFAULT_TEMPLATES = {
- soul: `# Soul
+ soul: `# Soul
_You're not a chatbot. You're becoming someone._
@@ -40,7 +40,7 @@ If you change this file, tell the user — it's your soul, and they should know.
_This file is yours to evolve. As you learn who you are, update it._
`,
- user: `# User
+ user: `# User
_Learn about the person you're helping. Update this as you go._
@@ -58,7 +58,7 @@ _(What do they care about? What projects are they working on? What annoys them?
The more you know, the better you can help. But remember — you're learning about a person, not building a dossier. Respect the difference.
`,
- workspace: `# Workspace
+ workspace: `# Workspace
This folder is home. Treat it that way.
@@ -127,7 +127,7 @@ Capture what matters. Decisions, context, things to remember.
This is a starting point. Add your own conventions, style, and rules as you figure out what works.
`,
- memory: `# Memory
+ memory: `# Memory
_(Persistent knowledge will be stored here. Update this as you learn.)_
diff --git a/src/agent/profile/types.ts b/src/agent/profile/types.ts
index 4565d7df..db313ceb 100644
--- a/src/agent/profile/types.ts
+++ b/src/agent/profile/types.ts
@@ -15,6 +15,8 @@ export const PROFILE_FILES = {
/** Profile config.json structure */
export interface ProfileConfig {
+ /** Agent display name */
+ name?: string;
/** Tools policy configuration */
tools?: ToolsConfig;
/** Default LLM provider */
diff --git a/src/agent/runner.ts b/src/agent/runner.ts
index bdc9e203..0efda920 100644
--- a/src/agent/runner.ts
+++ b/src/agent/runner.ts
@@ -446,12 +446,15 @@ export class Agent {
reloadTools(): string[] {
// Re-read profile tools config to get latest changes
const profileToolsConfig = this.profile?.getToolsConfig();
+ console.log(`[Agent] reloadTools: profileToolsConfig =`, JSON.stringify(profileToolsConfig));
const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, this.originalToolsConfig);
+ console.log(`[Agent] reloadTools: mergedToolsConfig =`, JSON.stringify(mergedToolsConfig));
this.toolsOptions = mergedToolsConfig
? { ...this.toolsOptions, tools: mergedToolsConfig }
: this.toolsOptions;
const tools = resolveTools(this.toolsOptions);
+ console.log(`[Agent] reloadTools: resolved ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`);
this.agent.setTools(tools);
if (this.debug) {
console.error(`[debug] Reloaded ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`);
@@ -532,4 +535,65 @@ export class Agent {
getProfileId(): string | undefined {
return this.profile?.getProfile()?.id;
}
+
+ /**
+ * Get agent display name from profile config.
+ */
+ getAgentName(): string | undefined {
+ return this.profile?.getName();
+ }
+
+ /**
+ * Update agent display name in profile config.
+ */
+ setAgentName(name: string): void {
+ this.profile?.updateName(name);
+ }
+
+ /**
+ * Get user.md content from profile.
+ */
+ getUserContent(): string | undefined {
+ return this.profile?.getUserContent();
+ }
+
+ /**
+ * Update user.md content in profile.
+ */
+ setUserContent(content: string): void {
+ this.profile?.updateUserContent(content);
+ }
+
+ /**
+ * Reload profile from disk and rebuild system prompt.
+ * Call this after updating profile files to apply changes immediately.
+ */
+ reloadSystemPrompt(): void {
+ if (!this.profile) {
+ console.log('[Agent] reloadSystemPrompt: no profile');
+ return;
+ }
+
+ // Reload profile from disk
+ console.log('[Agent] Reloading profile from disk...');
+ this.profile.reloadProfile();
+
+ // Rebuild system prompt
+ let systemPrompt = this.profile.buildSystemPrompt();
+ console.log('[Agent] Built system prompt, length:', systemPrompt?.length);
+
+ // Re-add skills prompt if skills are enabled
+ if (this.skillManager) {
+ const skillsPrompt = this.skillManager.buildModelSkillsPrompt();
+ if (skillsPrompt) {
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillsPrompt}` : skillsPrompt;
+ }
+ }
+
+ // Apply new system prompt
+ if (systemPrompt) {
+ console.log('[Agent] Applying system prompt, first 200 chars:', systemPrompt.substring(0, 200));
+ this.agent.setSystemPrompt(systemPrompt);
+ }
+ }
}