{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "agent-work", "title": "Agent Work", "description": "Controlled Chat and IFileSystem workspace composition with bounded AGENTS and explicit Skill context, read-only tools, responsive panes, and stable FileManager refresh", "dependencies": [ "@wener/ai@^0.1.21", "@wener/common@^3.0.0", "ai@7.0.31", "daisyui", "lucide-react", "react-resizable-panels@^4.12.1", "zod@4.4.3" ], "registryDependencies": ["https://ui-components.wener.me/r/file-manager.json"], "files": [ { "path": "registry/default/blocks/agent-work/workspace-types.ts", "content": "import type { Skill } from '@wener/ai/agent/skill';\nimport type { IFileSystem } from '@wener/common/fs';\n\nexport type AgentWorkspace = {\n\tid: string;\n\tfileSystem: IFileSystem;\n\trootPath: string;\n\tlabel?: string;\n\trevision?: number | string;\n};\n\nexport type AgentWorkspaceContextLimits = {\n\tmaxAgentsBytes: number;\n\tmaxAggregateBytes: number;\n\tmaxSkillBytes: number;\n\tmaxSkills: number;\n};\n\nexport const defaultAgentWorkspaceContextLimits: Readonly = Object.freeze({\n\tmaxAgentsBytes: 64 * 1024,\n\tmaxAggregateBytes: 256 * 1024,\n\tmaxSkillBytes: 32 * 1024,\n\tmaxSkills: 32,\n});\n\nexport type AgentWorkspaceContextIssue = {\n\tcode:\n\t\t| 'aborted'\n\t\t| 'agents-read-failed'\n\t\t| 'agents-too-large'\n\t\t| 'aggregate-too-large'\n\t\t| 'invalid-text'\n\t\t| 'skill-count-exceeded'\n\t\t| 'skill-too-large';\n\tmessage: string;\n\tpath?: string;\n\tskillIdentity?: string;\n\tskillName?: string;\n};\n\nexport type AgentWorkspaceAgentsContext = {\n\tbytes: number;\n\tcontent: string;\n\tpath: string;\n};\n\nexport type AgentWorkspaceSkillContext = Pick & {\n\tbytes: number;\n\tidentity: string;\n};\n\nexport type AgentWorkspaceContext = {\n\tagents?: AgentWorkspaceAgentsContext;\n\tbytes: number;\n\tissues: AgentWorkspaceContextIssue[];\n\tskills: AgentWorkspaceSkillContext[];\n\tworkspaceId: string;\n\tworkspaceRevision?: number | string;\n};\n\nexport type LoadAgentWorkspaceContextOptions = {\n\tagentsPath?: string;\n\tlimits?: Partial;\n\tsignal?: AbortSignal;\n\tskills: readonly Skill[];\n\tworkspace: AgentWorkspace;\n};\n", "type": "registry:lib", "target": "@components/blocks/agent-work/workspace-types.ts" }, { "path": "registry/default/blocks/agent-work/workspace-path.ts", "content": "const maxWorkspacePathBytes = 4096;\nconst maxWorkspaceNameBytes = 255;\n\nexport class AgentWorkspacePathError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly code: 'invalid-name' | 'invalid-path' | 'outside-root',\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'AgentWorkspacePathError';\n\t}\n}\n\nexport function normalizeAgentWorkspaceRoot(rootPath: string): string {\n\tif (!rootPath.startsWith('/')) throw new AgentWorkspacePathError('工作区根路径必须是绝对路径。', 'invalid-path');\n\treturn normalizeAbsolutePath(rootPath);\n}\n\nexport function resolveAgentWorkspacePath(rootPath: string, inputPath: string): string {\n\tconst root = normalizeAgentWorkspaceRoot(rootPath);\n\tif (!inputPath || utf8Bytes(inputPath) > maxWorkspacePathBytes || inputPath.includes('\\0')) {\n\t\tthrow new AgentWorkspacePathError('工作区路径无效或过长。', 'invalid-path');\n\t}\n\tconst candidate = inputPath.startsWith('/') ? normalizeAbsolutePath(inputPath) : joinPath(root, inputPath);\n\tif (!isAgentWorkspacePathWithinRoot(candidate, root)) {\n\t\tthrow new AgentWorkspacePathError('路径超出工作区根目录。', 'outside-root');\n\t}\n\treturn candidate;\n}\n\nexport function isAgentWorkspacePathWithinRoot(path: string, rootPath: string): boolean {\n\tconst root = normalizeAgentWorkspaceRoot(rootPath);\n\tconst candidate = normalizeAbsolutePath(path);\n\treturn root === '/' || candidate === root || candidate.startsWith(`${root}/`);\n}\n\nexport function validateAgentWorkspaceBasename(name: string): string {\n\tif (\n\t\t!name ||\n\t\tname === '.' ||\n\t\tname === '..' ||\n\t\tname.includes('/') ||\n\t\tname.includes('\\\\') ||\n\t\tname.includes('\\0') ||\n\t\tutf8Bytes(name) > maxWorkspaceNameBytes\n\t) {\n\t\tthrow new AgentWorkspacePathError('文件系统返回了无效名称。', 'invalid-name');\n\t}\n\treturn name;\n}\n\nexport function joinAgentWorkspaceEntryPath(directory: string, name: string): string {\n\tconst basename = validateAgentWorkspaceBasename(name);\n\treturn directory === '/' ? `/${basename}` : `${directory}/${basename}`;\n}\n\nfunction joinPath(root: string, relative: string): string {\n\treturn normalizeAbsolutePath(root === '/' ? `/${relative}` : `${root}/${relative}`);\n}\n\nfunction normalizeAbsolutePath(path: string): string {\n\tif (!path.startsWith('/') || path.includes('\\\\') || path.includes('\\0')) {\n\t\tthrow new AgentWorkspacePathError('工作区路径无效。', 'invalid-path');\n\t}\n\tconst segments: string[] = [];\n\tfor (const segment of path.split('/')) {\n\t\tif (!segment || segment === '.') continue;\n\t\tif (segment === '..') segments.pop();\n\t\telse segments.push(segment);\n\t}\n\tconst normalized = `/${segments.join('/')}`;\n\tif (utf8Bytes(normalized) > maxWorkspacePathBytes) {\n\t\tthrow new AgentWorkspacePathError('工作区路径过长。', 'invalid-path');\n\t}\n\treturn normalized;\n}\n\nfunction utf8Bytes(value: string): number {\n\treturn new TextEncoder().encode(value).byteLength;\n}\n", "type": "registry:lib", "target": "@components/blocks/agent-work/workspace-path.ts" }, { "path": "registry/default/blocks/agent-work/workspace-context.ts", "content": "import { getSkillIdentity, type Skill } from '@wener/ai/agent/skill';\nimport { resolveAgentWorkspacePath } from './workspace-path';\nimport {\n\ttype AgentWorkspaceContext,\n\ttype AgentWorkspaceContextIssue,\n\ttype AgentWorkspaceContextLimits,\n\tdefaultAgentWorkspaceContextLimits,\n\ttype LoadAgentWorkspaceContextOptions,\n} from './workspace-types';\n\nexport async function loadAgentWorkspaceContext({\n\tagentsPath = 'AGENTS.md',\n\tlimits: inputLimits,\n\tsignal,\n\tskills,\n\tworkspace,\n}: LoadAgentWorkspaceContextOptions): Promise {\n\tconst limits = resolveContextLimits(inputLimits);\n\tthrowIfAborted(signal);\n\tconst issues: AgentWorkspaceContextIssue[] = [];\n\tlet aggregateBytes = 0;\n\tlet agents: AgentWorkspaceContext['agents'];\n\tconst resolvedAgentsPath = resolveAgentWorkspacePath(workspace.rootPath, agentsPath);\n\ttry {\n\t\tconst bytes = await workspace.fileSystem.readFile(resolvedAgentsPath, {\n\t\t\tencoding: 'binary',\n\t\t\tmaxBytes: limits.maxAgentsBytes + 1,\n\t\t\tsignal,\n\t\t});\n\t\tthrowIfAborted(signal);\n\t\tif (bytes.byteLength > limits.maxAgentsBytes) {\n\t\t\tissues.push({\n\t\t\t\tcode: 'agents-too-large',\n\t\t\t\tmessage: `AGENTS 文件超过 ${limits.maxAgentsBytes} 字节,已整体排除。`,\n\t\t\t\tpath: resolvedAgentsPath,\n\t\t\t});\n\t\t} else if (bytes.byteLength > limits.maxAggregateBytes) {\n\t\t\tissues.push({\n\t\t\t\tcode: 'aggregate-too-large',\n\t\t\t\tmessage: 'AGENTS 文件超过工作区上下文总量限制,已整体排除。',\n\t\t\t\tpath: resolvedAgentsPath,\n\t\t\t});\n\t\t} else {\n\t\t\tconst content = decodeUtf8(bytes, resolvedAgentsPath, issues);\n\t\t\tif (content !== undefined) {\n\t\t\t\taggregateBytes += bytes.byteLength;\n\t\t\t\tagents = { bytes: bytes.byteLength, content, path: resolvedAgentsPath };\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tif (isAbortError(error) || signal?.aborted) throw createAbortError();\n\t\tif (!isNotFoundError(error)) {\n\t\t\tissues.push({\n\t\t\t\tcode: 'agents-read-failed',\n\t\t\t\tmessage: `读取 AGENTS 文件失败:${toErrorMessage(error)}`,\n\t\t\t\tpath: resolvedAgentsPath,\n\t\t\t});\n\t\t}\n\t}\n\n\tconst selectedSkills: AgentWorkspaceContext['skills'] = [];\n\tif (skills.length > limits.maxSkills) {\n\t\tissues.push({\n\t\t\tcode: 'skill-count-exceeded',\n\t\t\tmessage: `最多加载 ${limits.maxSkills} 个 Skill,其余已排除。`,\n\t\t});\n\t}\n\tfor (const skill of skills.slice(0, limits.maxSkills)) {\n\t\tthrowIfAborted(signal);\n\t\tconst descriptionBytes = utf8Bytes(skill.description);\n\t\tconst instructionBytes = utf8Bytes(skill.instructions);\n\t\tconst bytes = descriptionBytes + instructionBytes;\n\t\tif (descriptionBytes > limits.maxSkillBytes || instructionBytes > limits.maxSkillBytes) {\n\t\t\tissues.push(\n\t\t\t\tskillIssue('skill-too-large', skill, `Skill 的描述或指令超过 ${limits.maxSkillBytes} 字节,已整体排除。`),\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\tif (aggregateBytes + bytes > limits.maxAggregateBytes) {\n\t\t\tissues.push(skillIssue('aggregate-too-large', skill, 'Skill 超过工作区上下文总量限制,已整体排除。'));\n\t\t\tcontinue;\n\t\t}\n\t\taggregateBytes += bytes;\n\t\tselectedSkills.push({\n\t\t\tbytes,\n\t\t\tdescription: skill.description,\n\t\t\tidentity: getSkillIdentity(skill),\n\t\t\tinstructions: skill.instructions,\n\t\t\tname: skill.name,\n\t\t\tversion: skill.version,\n\t\t});\n\t}\n\tthrowIfAborted(signal);\n\treturn {\n\t\tagents,\n\t\tbytes: aggregateBytes,\n\t\tissues,\n\t\tskills: selectedSkills,\n\t\tworkspaceId: workspace.id,\n\t\tworkspaceRevision: workspace.revision,\n\t};\n}\n\nfunction resolveContextLimits(input?: Partial): AgentWorkspaceContextLimits {\n\tconst limits = { ...defaultAgentWorkspaceContextLimits, ...input };\n\tfor (const [name, value] of Object.entries(limits)) {\n\t\tif (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${name} 必须是正整数。`);\n\t}\n\treturn limits;\n}\n\nfunction skillIssue(\n\tcode: 'aggregate-too-large' | 'skill-too-large',\n\tskill: Skill,\n\tmessage: string,\n): AgentWorkspaceContextIssue {\n\treturn { code, message, skillIdentity: getSkillIdentity(skill), skillName: skill.name };\n}\n\nfunction decodeUtf8(bytes: Uint8Array, path: string, issues: AgentWorkspaceContextIssue[]): string | undefined {\n\ttry {\n\t\treturn new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n\t} catch {\n\t\tissues.push({ code: 'invalid-text', message: 'AGENTS 文件不是有效的 UTF-8 文本,已整体排除。', path });\n\t\treturn undefined;\n\t}\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n\treturn typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof DOMException\n\t\t? error.name === 'AbortError'\n\t\t: error instanceof Error && error.name === 'AbortError';\n}\n\nfunction throwIfAborted(signal?: AbortSignal): void {\n\tif (signal?.aborted) throw createAbortError();\n}\n\nfunction createAbortError(): Error {\n\treturn typeof DOMException === 'function'\n\t\t? new DOMException('上下文加载已取消。', 'AbortError')\n\t\t: Object.assign(new Error('上下文加载已取消。'), { name: 'AbortError' });\n}\n\nfunction toErrorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction utf8Bytes(value: string): number {\n\treturn new TextEncoder().encode(value).byteLength;\n}\n", "type": "registry:lib", "target": "@components/blocks/agent-work/workspace-context.ts" }, { "path": "registry/default/blocks/agent-work/workspace-mutation-guard.ts", "content": "import { FileSystemError, type IFileSystem } from '@wener/common/fs';\n\nexport function createAgentWorkspaceMutationGuard(fileSystem: IFileSystem, canMutate: () => boolean): IFileSystem {\n\tconst assertMutationAllowed = () => {\n\t\tif (!canMutate()) throw new FileSystemError('Workspace mutations are quarantined', 'EPERM');\n\t};\n\treturn {\n\t\tcopy: async (...args) => {\n\t\t\tassertMutationAllowed();\n\t\t\tawait fileSystem.copy(...args);\n\t\t},\n\t\tcreateReadableStream: fileSystem.createReadableStream?.bind(fileSystem),\n\t\tcreateWritableStream: fileSystem.createWritableStream\n\t\t\t? (...args) => {\n\t\t\t\t\tassertMutationAllowed();\n\t\t\t\t\tconst writer = fileSystem.createWritableStream!(...args).getWriter();\n\t\t\t\t\treturn new WritableStream({\n\t\t\t\t\t\tabort: (reason) => writer.abort(reason),\n\t\t\t\t\t\tclose: async () => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tassertMutationAllowed();\n\t\t\t\t\t\t\t\tawait writer.close();\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tawait writer.abort(error).catch(() => undefined);\n\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\twrite: async (chunk) => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tassertMutationAllowed();\n\t\t\t\t\t\t\t\tawait writer.write(chunk);\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tawait writer.abort(error).catch(() => undefined);\n\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t: undefined,\n\t\texists: (...args) => fileSystem.exists(...args),\n\t\tgetUrl: fileSystem.getUrl?.bind(fileSystem),\n\t\tmkdir: async (...args) => {\n\t\t\tassertMutationAllowed();\n\t\t\tawait fileSystem.mkdir(...args);\n\t\t},\n\t\treadFile: fileSystem.readFile.bind(fileSystem),\n\t\treaddir: (...args) => fileSystem.readdir(...args),\n\t\trename: async (...args) => {\n\t\t\tassertMutationAllowed();\n\t\t\tawait fileSystem.rename(...args);\n\t\t},\n\t\trm: async (...args) => {\n\t\t\tassertMutationAllowed();\n\t\t\tawait fileSystem.rm(...args);\n\t\t},\n\t\tstat: (...args) => fileSystem.stat(...args),\n\t\twriteFile: async (...args) => {\n\t\t\tassertMutationAllowed();\n\t\t\tawait fileSystem.writeFile(...args);\n\t\t},\n\t};\n}\n", "type": "registry:lib", "target": "@components/blocks/agent-work/workspace-mutation-guard.ts" }, { "path": "registry/default/blocks/agent-work/instruction-composer.ts", "content": "import type { Persona } from '@wener/ai/agent/persona';\nimport type { AgentWorkspaceContext, AgentWorkspaceContextIssue } from './workspace-types';\n\nexport const AgentInstructionSeparator = '\\n\\n---\\n\\n';\nexport const defaultAgentInstructionMaxBytes = 256 * 1024;\n\nexport type ComposeAgentInstructionsOptions = {\n\tcontext?: AgentWorkspaceContext;\n\tmaxBytes?: number;\n\tpersona?: Persona;\n};\n\nexport type ComposedAgentInstructions = {\n\tbytes: number;\n\tincluded: Array<'agents' | 'persona' | `skill:${string}`>;\n\tinstructions: string;\n\tissues: AgentWorkspaceContextIssue[];\n};\n\nexport function composeAgentInstructions({\n\tcontext,\n\tmaxBytes = defaultAgentInstructionMaxBytes,\n\tpersona,\n}: ComposeAgentInstructionsOptions): ComposedAgentInstructions {\n\tif (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error('指令总量限制必须是正整数。');\n\tconst sections: string[] = [];\n\tconst included: ComposedAgentInstructions['included'] = [];\n\tconst issues = [...(context?.issues ?? [])];\n\tlet bytes = 0;\n\tconst append = (kind: ComposedAgentInstructions['included'][number], content: string, label: string) => {\n\t\tif (!content.trim()) return;\n\t\tconst section = `## ${label}\\n\\n${content.trim()}`;\n\t\tconst addition = `${sections.length ? AgentInstructionSeparator : ''}${section}`;\n\t\tconst additionBytes = utf8Bytes(addition);\n\t\tif (bytes + additionBytes > maxBytes) {\n\t\t\tissues.push({ code: 'aggregate-too-large', message: `${label} 超过指令总量限制,已整体排除。` });\n\t\t\treturn;\n\t\t}\n\t\tsections.push(section);\n\t\tincluded.push(kind);\n\t\tbytes += additionBytes;\n\t};\n\n\tif (persona) append('persona', composePersona(persona), `Persona: ${persona.name}`);\n\tif (context?.agents) append('agents', context.agents.content, `AGENTS: ${context.agents.path}`);\n\tfor (const skill of context?.skills ?? []) {\n\t\tappend(\n\t\t\t`skill:${skill.identity}`,\n\t\t\t`描述:${skill.description}\\n\\n指令:\\n${skill.instructions}`,\n\t\t\t`Skill: ${skill.name}@${skill.version}`,\n\t\t);\n\t}\n\treturn { bytes, included, instructions: sections.join(AgentInstructionSeparator), issues };\n}\n\nfunction composePersona(persona: Persona): string {\n\tconst parts: string[] = [];\n\tappendLabeled(parts, '描述', persona.description);\n\tappendLabeled(parts, 'System', persona.prompts?.system);\n\tappendLabeled(parts, 'Persona', persona.prompts?.persona);\n\tappendLabeled(parts, '场景', persona.prompts?.scenario);\n\tappendLabeled(parts, '对话后指令', persona.prompts?.postHistoryInstructions);\n\tappendLabeled(parts, '背景', persona.authoring?.background);\n\tappendLabeled(parts, '性格', persona.authoring?.personality);\n\tappendLabeled(parts, '行为策略', persona.authoring?.behaviorPolicy);\n\tappendLabeled(parts, '表达风格', persona.authoring?.speechStyle);\n\tif (persona.authoring?.values?.length) parts.push(`价值观:${persona.authoring.values.join(';')}`);\n\tif (persona.authoring?.goals?.length) parts.push(`目标:${persona.authoring.goals.join(';')}`);\n\treturn parts.join('\\n\\n');\n}\n\nfunction appendLabeled(target: string[], label: string, value?: string): void {\n\tif (value?.trim()) target.push(`${label}:${value.trim()}`);\n}\n\nfunction utf8Bytes(value: string): number {\n\treturn new TextEncoder().encode(value).byteLength;\n}\n", "type": "registry:lib", "target": "@components/blocks/agent-work/instruction-composer.ts" }, { "path": "registry/default/blocks/agent-work/workspace-tools.ts", "content": "import { type ToolSet, tool } from 'ai';\nimport { z } from 'zod';\nimport {\n\tjoinAgentWorkspaceEntryPath,\n\tresolveAgentWorkspacePath,\n\tvalidateAgentWorkspaceBasename,\n} from './workspace-path';\nimport type { AgentWorkspace } from './workspace-types';\n\nexport const MaxAgentWorkspaceListEntries = 200;\nexport const MaxAgentWorkspaceListOutputBytes = 64 * 1024;\nexport const MaxAgentWorkspaceReadBytes = 64 * 1024;\n\nexport function createAgentWorkspaceReadTools(workspace: AgentWorkspace): ToolSet {\n\treturn {\n\t\tworkspace_list: tool({\n\t\t\tdescription: '列出工作区根目录内的文件和目录。',\n\t\t\tinputSchema: z.object({ path: z.string().max(4096).default('.') }),\n\t\t\texecute: async ({ path }, { abortSignal }) => listWorkspace(workspace, path, abortSignal),\n\t\t}),\n\t\tworkspace_read: tool({\n\t\t\tdescription: '读取工作区根目录内不超过 64 KiB 的 UTF-8 文件。',\n\t\t\tinputSchema: z.object({ path: z.string().min(1).max(4096) }),\n\t\t\texecute: async ({ path }, { abortSignal }) => readWorkspace(workspace, path, abortSignal),\n\t\t}),\n\t};\n}\n\nasync function listWorkspace(workspace: AgentWorkspace, inputPath: string, signal?: AbortSignal) {\n\ttry {\n\t\tconst path = resolveAgentWorkspacePath(workspace.rootPath, inputPath);\n\t\tconst rawEntries = await workspace.fileSystem.readdir(path, {\n\t\t\tmaxEntries: MaxAgentWorkspaceListEntries,\n\t\t\tsignal,\n\t\t});\n\t\tconst entries: Array<{ kind: 'directory' | 'file'; name: string; size: number }> = [];\n\t\tconst seen = new Set();\n\t\tlet invalidEntries = 0;\n\t\tlet truncated = rawEntries.length > MaxAgentWorkspaceListEntries;\n\t\tfor (const raw of rawEntries.slice(0, MaxAgentWorkspaceListEntries)) {\n\t\t\tif (entries.length >= MaxAgentWorkspaceListEntries) break;\n\t\t\ttry {\n\t\t\t\tconst name = validateAgentWorkspaceBasename(raw.name);\n\t\t\t\tjoinAgentWorkspaceEntryPath(path, name);\n\t\t\t\tif (seen.has(name) || (raw.kind !== 'directory' && raw.kind !== 'file')) throw new Error('invalid entry');\n\t\t\t\tseen.add(name);\n\t\t\t\tconst next = { kind: raw.kind, name, size: safeSize(raw.size) };\n\t\t\t\tconst candidate = { entries: [...entries, next], invalidEntries, ok: true, path, truncated };\n\t\t\t\tif (jsonBytes(candidate) > MaxAgentWorkspaceListOutputBytes) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tentries.push(next);\n\t\t\t} catch {\n\t\t\t\tinvalidEntries += 1;\n\t\t\t}\n\t\t}\n\t\tentries.sort((left, right) => left.name.localeCompare(right.name));\n\t\treturn { entries, invalidEntries, ok: true as const, path, truncated };\n\t} catch (error) {\n\t\treturn toolError(error);\n\t}\n}\n\nasync function readWorkspace(workspace: AgentWorkspace, inputPath: string, signal?: AbortSignal) {\n\ttry {\n\t\tconst path = resolveAgentWorkspacePath(workspace.rootPath, inputPath);\n\t\tconst content = await workspace.fileSystem.readFile(path, {\n\t\t\tencoding: 'binary',\n\t\t\tmaxBytes: MaxAgentWorkspaceReadBytes + 1,\n\t\t\tsignal,\n\t\t});\n\t\tif (content.byteLength > MaxAgentWorkspaceReadBytes) {\n\t\t\treturn { code: 'too-large' as const, limitBytes: MaxAgentWorkspaceReadBytes, ok: false as const, path };\n\t\t}\n\t\ttry {\n\t\t\treturn {\n\t\t\t\tbytes: content.byteLength,\n\t\t\t\tcontent: new TextDecoder('utf-8', { fatal: true }).decode(content),\n\t\t\t\tok: true as const,\n\t\t\t\tpath,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn { code: 'binary' as const, ok: false as const, path };\n\t\t}\n\t} catch (error) {\n\t\treturn toolError(error);\n\t}\n}\n\nfunction toolError(error: unknown) {\n\tif (error instanceof Error && error.name === 'AbortError') return { code: 'aborted' as const, ok: false as const };\n\tconst code =\n\t\ttypeof error === 'object' && error !== null && 'code' in error ? String(error.code).slice(0, 64) : 'failed';\n\treturn { code, message: toErrorMessage(error).slice(0, 256), ok: false as const };\n}\n\nfunction toErrorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : '工作区操作失败。';\n}\n\nfunction jsonBytes(value: unknown): number {\n\treturn new TextEncoder().encode(JSON.stringify(value)).byteLength;\n}\n\nfunction safeSize(value: number): number {\n\treturn Number.isSafeInteger(value) && value >= 0 ? value : 0;\n}\n", "type": "registry:lib", "target": "@components/blocks/agent-work/workspace-tools.ts" }, { "path": "registry/default/blocks/agent-work/use-agent-workspace-context.ts", "content": "'use client';\n\nimport type { Skill } from '@wener/ai/agent/skill';\nimport { useEffect, useRef, useState } from 'react';\nimport { loadAgentWorkspaceContext } from './workspace-context';\nimport type { AgentWorkspace, AgentWorkspaceContext, AgentWorkspaceContextLimits } from './workspace-types';\n\nexport type UseAgentWorkspaceContextOptions = {\n\tagentsPath?: string;\n\tenabled?: boolean;\n\tlimits?: Partial;\n\tskills: readonly Skill[];\n\tworkspace: AgentWorkspace;\n};\n\nexport type AgentWorkspaceContextState = {\n\tcontext?: AgentWorkspaceContext;\n\terror?: Error;\n\tstatus: 'error' | 'idle' | 'loading' | 'ready';\n};\n\nexport function useAgentWorkspaceContext(options: UseAgentWorkspaceContextOptions): AgentWorkspaceContextState {\n\tconst [state, setState] = useState({\n\t\tstatus: options.enabled === false ? 'idle' : 'loading',\n\t});\n\tconst stableOptions = useStableContextOptions(options);\n\tuseEffect(() => {\n\t\tif (stableOptions.enabled === false) {\n\t\t\tsetState({ status: 'idle' });\n\t\t\treturn;\n\t\t}\n\t\tconst controller = new AbortController();\n\t\tlet current = true;\n\t\tsetState({ status: 'loading' });\n\t\tvoid loadAgentWorkspaceContext({ ...stableOptions, signal: controller.signal }).then(\n\t\t\t(context) => {\n\t\t\t\tif (current && !controller.signal.aborted) setState({ context, status: 'ready' });\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\tif (current && !controller.signal.aborted) {\n\t\t\t\t\tsetState({ error: error instanceof Error ? error : new Error(String(error)), status: 'error' });\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t\treturn () => {\n\t\t\tcurrent = false;\n\t\t\tcontroller.abort();\n\t\t};\n\t}, [stableOptions]);\n\treturn state;\n}\n\nfunction useStableContextOptions(options: UseAgentWorkspaceContextOptions): UseAgentWorkspaceContextOptions {\n\tconst stable = useRef(options);\n\tif (!sameContextOptions(stable.current, options)) stable.current = options;\n\treturn stable.current;\n}\n\nfunction sameContextOptions(left: UseAgentWorkspaceContextOptions, right: UseAgentWorkspaceContextOptions): boolean {\n\treturn (\n\t\tleft.agentsPath === right.agentsPath &&\n\t\tleft.enabled === right.enabled &&\n\t\tleft.workspace.id === right.workspace.id &&\n\t\tleft.workspace.fileSystem === right.workspace.fileSystem &&\n\t\tleft.workspace.rootPath === right.workspace.rootPath &&\n\t\tleft.workspace.revision === right.workspace.revision &&\n\t\tsameLimits(left.limits, right.limits) &&\n\t\tleft.skills.length === right.skills.length &&\n\t\tleft.skills.every((skill, index) => skill === right.skills[index])\n\t);\n}\n\nfunction sameLimits(\n\tleft: Partial | undefined,\n\tright: Partial | undefined,\n): boolean {\n\treturn (\n\t\tleft?.maxAgentsBytes === right?.maxAgentsBytes &&\n\t\tleft?.maxAggregateBytes === right?.maxAggregateBytes &&\n\t\tleft?.maxSkillBytes === right?.maxSkillBytes &&\n\t\tleft?.maxSkills === right?.maxSkills\n\t);\n}\n", "type": "registry:hook", "target": "@components/blocks/agent-work/use-agent-workspace-context.ts" }, { "path": "registry/default/blocks/agent-work/agent-work.tsx", "content": "'use client';\n\nimport { Files, MessagesSquare } from 'lucide-react';\nimport {\n\ttype ComponentPropsWithRef,\n\ttype KeyboardEvent,\n\ttype ReactNode,\n\tuseEffect,\n\tuseId,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from 'react';\nimport { FileManager, type FileManagerProps } from '@/components/file-manager/file-manager';\nimport { createFileManagerStore } from '@/components/file-manager/file-manager-store';\nimport type { FileManagerStore } from '@/components/file-manager/file-manager-types';\nimport { cn } from '@/lib/utils';\nimport { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../ui/resizable';\nimport type { AgentWorkspace, AgentWorkspaceContext } from './workspace-types';\n\nexport type AgentWorkPane = 'chat' | 'workspace';\n\nexport type AgentWorkTab = {\n\tid: string;\n\tlabel: ReactNode;\n\tpanel: ReactNode;\n};\n\nexport type AgentWorkMessages = {\n\tchatPane: string;\n\tcontextTab: string;\n\tfilesTab: string;\n\tworkspacePane: string;\n};\n\nexport const defaultAgentWorkMessages: AgentWorkMessages = {\n\tchatPane: '对话',\n\tcontextTab: '上下文',\n\tfilesTab: '文件',\n\tworkspacePane: '工作区',\n};\n\nexport type AgentWorkProps = ComponentPropsWithRef<'section'> & {\n\tchat: ReactNode;\n\tcontext?: AgentWorkspaceContext;\n\tdefaultPane?: AgentWorkPane;\n\tdefaultWorkspaceTab?: string;\n\textraWorkspaceTabs?: readonly AgentWorkTab[];\n\tfileManagerProps?: Omit;\n\tmessages?: Partial;\n\tonPaneChange?: (pane: AgentWorkPane) => void;\n\tonWorkspaceTabChange?: (tab: string) => void;\n\tpane?: AgentWorkPane;\n\tworkspace: AgentWorkspace;\n\tworkspaceTab?: string;\n\tworkspaceReadOnly?: boolean;\n};\n\nexport function AgentWork({\n\tchat,\n\tclassName,\n\tcontext,\n\tdefaultPane = 'chat',\n\tdefaultWorkspaceTab = 'files',\n\textraWorkspaceTabs = [],\n\tfileManagerProps,\n\tmessages,\n\tonPaneChange,\n\tonWorkspaceTabChange,\n\tpane,\n\tworkspace,\n\tworkspaceTab,\n\tworkspaceReadOnly = false,\n\t...props\n}: AgentWorkProps) {\n\tconst copy = { ...defaultAgentWorkMessages, ...messages };\n\tconst rootRef = useRef(null);\n\tconst [narrow, setNarrow] = useState(false);\n\tconst [localPane, setLocalPane] = useState(defaultPane);\n\tconst [localTab, setLocalTab] = useState(defaultWorkspaceTab);\n\tconst visiblePane = pane ?? localPane;\n\tconst visibleTab = workspaceTab ?? localTab;\n\tconst fileStore = useAgentWorkFileStore(workspace, fileManagerProps?.store);\n\tconst tabs = useMemo(\n\t\t() => [\n\t\t\t{\n\t\t\t\tid: 'files',\n\t\t\t\tlabel: copy.filesTab,\n\t\t\t\tpanel: (\n\t\t\t\t\t\n\t\t\t\t),\n\t\t\t},\n\t\t\t{ id: 'context', label: copy.contextTab, panel: },\n\t\t\t...extraWorkspaceTabs,\n\t\t],\n\t\t[\n\t\t\tcontext,\n\t\t\tcopy.contextTab,\n\t\t\tcopy.filesTab,\n\t\t\textraWorkspaceTabs,\n\t\t\tfileManagerProps,\n\t\t\tfileStore,\n\t\t\tworkspace,\n\t\t\tworkspaceReadOnly,\n\t\t],\n\t);\n\tconst activeTab = tabs.find((tab) => tab.id === visibleTab) ?? tabs[0];\n\n\tuseEffect(() => {\n\t\tconst element = rootRef.current;\n\t\tif (!element) return;\n\t\tconst update = () => setNarrow(element.getBoundingClientRect().width < 768);\n\t\tupdate();\n\t\tif (typeof ResizeObserver !== 'function') return;\n\t\tconst observer = new ResizeObserver(update);\n\t\tobserver.observe(element);\n\t\treturn () => observer.disconnect();\n\t}, []);\n\n\tconst selectPane = (next: AgentWorkPane) => {\n\t\tif (pane === undefined) setLocalPane(next);\n\t\tonPaneChange?.(next);\n\t};\n\tconst selectTab = (next: string) => {\n\t\tif (workspaceTab === undefined) setLocalTab(next);\n\t\tonWorkspaceTabChange?.(next);\n\t};\n\tconst workspacePanel = (\n\t\t\n\t\t\t{activeTab.panel}\n\t\t\n\t);\n\n\treturn (\n\t\t\n\t\t\t{narrow ? (\n\t\t\t\t<>\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t工作模式面板\n\t\t\t\t\t\t\t} onClick={() => selectPane('chat')}>\n\t\t\t\t\t\t\t\t{copy.chatPane}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t} onClick={() => selectPane('workspace')}>\n\t\t\t\t\t\t\t\t{copy.workspacePane}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t
{visiblePane === 'chat' ? chat : workspacePanel}
\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t
{chat}
\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{workspacePanel}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t)}\n\t\t\n\t);\n}\n\nfunction AgentWorkspacePanel({\n\tactiveTab,\n\tchildren,\n\tonTabChange,\n\ttabs,\n}: {\n\tactiveTab: string;\n\tchildren: ReactNode;\n\tonTabChange: (tab: string) => void;\n\ttabs: readonly AgentWorkTab[];\n}) {\n\tconst tabPanelId = useId();\n\tconst tabRefs = useRef(new Map());\n\tconst activeIndex = Math.max(\n\t\t0,\n\t\ttabs.findIndex((tab) => tab.id === activeTab),\n\t);\n\tconst activeTabId = `${tabPanelId}-tab-${activeIndex}`;\n\tconst handleKeyDown = (event: KeyboardEvent, index: number) => {\n\t\tlet nextIndex = index;\n\t\tif (event.key === 'ArrowRight') nextIndex = (index + 1) % tabs.length;\n\t\telse if (event.key === 'ArrowLeft') nextIndex = (index - 1 + tabs.length) % tabs.length;\n\t\telse if (event.key === 'Home') nextIndex = 0;\n\t\telse if (event.key === 'End') nextIndex = tabs.length - 1;\n\t\telse return;\n\t\tevent.preventDefault();\n\t\tconst next = tabs[nextIndex];\n\t\tif (!next) return;\n\t\tonTabChange(next.id);\n\t\ttabRefs.current.get(next.id)?.focus();\n\t};\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{tabs.map((tab, index) => (\n\t\t\t\t\t {\n\t\t\t\t\t\t\tif (element) tabRefs.current.set(tab.id, element);\n\t\t\t\t\t\t\telse tabRefs.current.delete(tab.id);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttype='button'\n\t\t\t\t\t\trole='tab'\n\t\t\t\t\t\taria-controls={tabPanelId}\n\t\t\t\t\t\taria-selected={activeTab === tab.id}\n\t\t\t\t\t\ttabIndex={activeTab === tab.id ? 0 : -1}\n\t\t\t\t\t\tclassName={cn('tab min-h-10', activeTab === tab.id && 'tab-active')}\n\t\t\t\t\t\tonKeyDown={(event) => handleKeyDown(event, index)}\n\t\t\t\t\t\tonClick={() => onTabChange(tab.id)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{tab.label}\n\t\t\t\t\t\n\t\t\t\t))}\n\t\t\t\n\t\t\t
\n\t\t\t\t{children}\n\t\t\t
\n\t\t
\n\t);\n}\n\nfunction AgentWorkContextPanel({ context }: { context?: AgentWorkspaceContext }) {\n\tif (!context) return
尚未加载工作区上下文。
;\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t

工作区上下文

\n\t\t\t\t

{context.bytes} 字节

\n\t\t\t
\n\t\t\t{context.agents ? (\n\t\t\t\t
\n\t\t\t\t\t

{context.agents.path}

\n\t\t\t\t\t
\n\t\t\t\t\t\t{context.agents.content}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t) : null}\n\t\t\t{context.skills.map((skill) => (\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t{skill.name}@{skill.version}\n\t\t\t\t\t

\n\t\t\t\t\t

{skill.description}

\n\t\t\t\t\t
\n\t\t\t\t\t\t{skill.instructions}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t))}\n\t\t\t{context.issues.length ? (\n\t\t\t\t
\n\t\t\t\t\t

上下文问题

\n\t\t\t\t\t
    \n\t\t\t\t\t\t{context.issues.map((issue, index) => (\n\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t{issue.message}\n\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t))}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t) : null}\n\t\t
\n\t);\n}\n\nfunction PaneButton({\n\tactive,\n\tchildren,\n\ticon,\n\tonClick,\n}: {\n\tactive: boolean;\n\tchildren: ReactNode;\n\ticon: ReactNode;\n\tonClick: () => void;\n}) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t{children}\n\t\t\n\t);\n}\n\nfunction useAgentWorkFileStore(workspace: AgentWorkspace, supplied?: FileManagerStore): FileManagerStore {\n\tconst [local] = useState(() =>\n\t\tcreateFileManagerStore({ fileSystem: workspace.fileSystem, rootPath: workspace.rootPath }),\n\t);\n\tconst store = supplied ?? local;\n\tconst identity = useRef({\n\t\tfileSystem: workspace.fileSystem,\n\t\tid: workspace.id,\n\t\trevision: workspace.revision,\n\t\trootPath: workspace.rootPath,\n\t});\n\tuseEffect(() => {\n\t\tconst previous = identity.current;\n\t\tconst backendChanged =\n\t\t\tprevious.fileSystem !== workspace.fileSystem ||\n\t\t\tprevious.id !== workspace.id ||\n\t\t\tprevious.rootPath !== workspace.rootPath;\n\t\tidentity.current = {\n\t\t\tfileSystem: workspace.fileSystem,\n\t\t\tid: workspace.id,\n\t\t\trevision: workspace.revision,\n\t\t\trootPath: workspace.rootPath,\n\t\t};\n\t\tif (backendChanged) store.getState().actions.replaceFileSystem(workspace.fileSystem, workspace.rootPath);\n\t\telse if (previous.revision !== workspace.revision) store.getState().actions.refresh();\n\t}, [store, workspace.fileSystem, workspace.id, workspace.revision, workspace.rootPath]);\n\treturn store;\n}\n", "type": "registry:component", "target": "@components/blocks/agent-work/agent-work.tsx" }, { "path": "registry/default/blocks/agent-work/index.ts", "content": "export * from './agent-work';\nexport * from './instruction-composer';\nexport * from './use-agent-workspace-context';\nexport * from './workspace-context';\nexport * from './workspace-mutation-guard';\nexport * from './workspace-path';\nexport * from './workspace-tools';\nexport * from './workspace-types';\n", "type": "registry:component", "target": "@components/blocks/agent-work/index.ts" } ], "type": "registry:block" }