feat: unify execution and add workspace file imports

Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
This commit is contained in:
mesalogo
2026-10-04 20:02:59 +08:00
parent 0cd5da33f4
commit 56ebd40bd8
374 changed files with 7075 additions and 6224 deletions
@@ -146,7 +146,7 @@ export type AttachPreface = z.infer<typeof attachPrefaceSchema>
export type AttachWelcome = z.infer<typeof attachWelcomeSchema>
export const RUNTIME_ACP_CAPABILITY_NAME = 'runtime/acp'
export const RUNTIME_ACP_CAPABILITY_VERSION = 5
export const RUNTIME_ACP_CAPABILITY_VERSION = 6
export const RUNTIME_MODEL_BRIDGE_CAPABILITY_NAME =
'runtime/model-bridge'
export const RUNTIME_MODEL_BRIDGE_CAPABILITY_VERSION = 1
+2 -7
View File
@@ -30,7 +30,6 @@ const untouchedProject: AssistantProject = {
kind: 'local',
rootPath: 'C:\\Workspace'
},
defaultWorkMode: 'ask',
kind: 'user',
builtInDefault: true,
status: 'active',
@@ -137,7 +136,6 @@ describe('isUntouchedBuiltInDefaultProject', () => {
isUntouchedBuiltInDefaultProject({
...untouchedProject,
rootPath: 'D:\\Moved',
defaultWorkMode: 'execute',
runtimeSelection: { provider: 'continue' },
status: 'archived',
updatedAt: '2026-08-01T00:00:01.000Z'
@@ -150,7 +148,6 @@ describe('isUntouchedBuiltInDefaultProject', () => {
name: builtInDefaultProjectSeedName,
description: builtInDefaultProjectSeedDescription,
rootPath: 'C:\\Workspace',
defaultWorkMode: 'ask',
builtInDefault: true
}
expect(projectCreateSchema.safeParse(input).success).toBe(false)
@@ -160,7 +157,7 @@ describe('isUntouchedBuiltInDefaultProject', () => {
describe('project execution space contracts', () => {
it.each(['', ' ', '\t\n'])('rejects blank creation paths but preserves updates: %j', (rootPath) => {
const input = { name: 'Project', description: '', rootPath, defaultWorkMode: 'ask' }
const input = { name: 'Project', description: '', rootPath }
const result = projectCreateSchema.safeParse(input)
expect(result.success).toBe(false)
if (!result.success) expect(result.error.issues[0]?.path).toEqual(['rootPath'])
@@ -169,7 +166,7 @@ describe('project execution space contracts', () => {
it.each(['C:\\Workspace', '/home/user/project'])('accepts and trims a creation directory: %s', (rootPath) => {
expect(projectCreateSchema.parse({
name: 'Project', description: '', rootPath: ` ${rootPath} `, defaultWorkMode: 'ask'
name: 'Project', description: '', rootPath: ` ${rootPath} `
}).rootPath).toBe(rootPath)
})
@@ -248,7 +245,6 @@ describe('project execution space contracts', () => {
name: '远程草稿',
description: '',
rootPath: '/srv/goodbuddy',
defaultWorkMode: 'ask',
executionSpace: {
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
@@ -496,7 +492,6 @@ describe('conversation persistence contracts', () => {
label: '编程 Subagent'
},
routingMode: 'native',
workMode: 'execute',
state: 'completed',
reason: '修复并验证聚焦变更',
output: '验证通过'
+2 -21
View File
@@ -25,13 +25,6 @@ export interface ExecutionStats {
/** Project card totals; empty for conversation queries. */
taskDurations: Array<{ id: string; durationMs: number; runningCount: number; incomplete: boolean }>
}
export const interactiveWorkModes = ['ask', 'execute'] as const
export const workModeSchema = z.enum(interactiveWorkModes)
export const legacyWorkModeSchema = z.enum([
'ask',
'plan',
'execute'
])
export const projectKindSchema = z.enum(['user', 'channel'])
export const projectChannels = [
'weixin',
@@ -45,9 +38,6 @@ export const projectChannelLabels: Record<ProjectChannel, string> = {
dingtalk: '钉钉'
}
export type WorkMode = z.infer<typeof workModeSchema>
export type LegacyWorkMode = z.infer<typeof legacyWorkModeSchema>
export type InteractiveWorkMode = (typeof interactiveWorkModes)[number]
export type ProjectKind = z.infer<typeof projectKindSchema>
export type ProjectChannel = z.infer<typeof projectChannelSchema>
@@ -270,18 +260,11 @@ export type ProjectExecutionSpace = z.output<
typeof persistedProjectExecutionSpaceSchema
>
export function normalizeInteractiveWorkMode(
workMode: LegacyWorkMode | undefined
): InteractiveWorkMode {
return workMode === 'execute' ? 'execute' : 'ask'
}
export const projectUpdateSchema = z
.object({
name: z.string().trim().min(1).max(120),
description: z.string().trim().max(2_000),
rootPath: z.string().trim().max(4_096),
defaultWorkMode: workModeSchema,
/** Project layer; absent means follow the global settings. */
runtimeSelection: runtimeSelectionLayerSchema.optional()
})
@@ -441,7 +424,6 @@ const conversationSubagentActivityBaseSchema = z.object({
childTaskId: assistantIdSchema,
routingMode: z.enum(['manual', 'smart', 'native']),
runtimeCallId: z.string().trim().min(1).max(256).optional(),
workMode: workModeSchema.optional(),
state: z.enum([
'queued',
'running',
@@ -724,7 +706,6 @@ export const conversationSnapshotSchema = z
/** Conversation layer; absent means follow the project. */
runtimeSelection: runtimeSelectionLayerSchema.optional(),
knowledgeLibraryIds: z.array(assistantIdSchema).max(20).optional(),
workMode: workModeSchema.optional(),
knowledgeRetrievalMode: z.enum(['auto', 'always']).optional(),
storyGraphEnabled: z.boolean().optional(),
contextMetrics: conversationContextMetricsSchema.optional(),
@@ -868,6 +849,8 @@ export type WorkspaceDirectoryEntry = {
name: string
path: string
type: 'file' | 'directory'
modifiedAt?: string
createdAt?: string
}
export type WorkspaceDirectoryListing = {
@@ -910,7 +893,6 @@ export type AssistantTask = {
instructions: string
origin: 'user' | 'assistant' | 'schedule' | 'delegation' | 'subagent'
status: AssistantTaskStatus
workMode?: WorkMode
progress?: number
createdAt: string
startedAt?: string
@@ -1027,7 +1009,6 @@ export const scheduleCreateSchema = z
conversationId: z.string().uuid().optional(),
title: z.string().trim().min(1).max(120),
prompt: z.string().trim().min(1).max(100_000),
workMode: workModeSchema.default('execute'),
recurrence: z.enum(['once', 'daily', 'weekly']),
nextRunAt: z.string().datetime({ offset: true }),
runImmediately: z.boolean().optional()
+1 -1
View File
@@ -69,7 +69,7 @@ export const builtinMcpServers = [
id: 'magic-notes',
name: '笔记',
description:
'读取全局魔法笔记,并在 Execute 模式下创建、修改或删除笔记与记录。',
'读取全局魔法笔记,创建、修改或删除笔记与记录。',
tools: magicNoteScopedDataTools.map(({ name, summary, access }) => ({
name,
description: summary,
+5 -5
View File
@@ -25,7 +25,7 @@ export const builtinModelTools = [
name: 'workspace_rg',
displayName: '搜索工作区',
description:
'使用内置 ripgrep 原生参数搜索或列出文件;Ask 限于工作区只读搜索,Execute 使用当前账号权限,长输出可分页续读。',
'使用内置 ripgrep 原生参数搜索或列出文件,使用当前账号权限,长输出可分页续读。',
access: 'read',
group: 'filesystem'
},
@@ -128,7 +128,7 @@ export const builtinModelTools = [
name: 'subagent_delegate',
displayName: '编程 Subagent',
description:
'将聚焦任务委派给同一模型连接,继承当前工作模式和工作区;长输出可分页续读。',
'将聚焦任务委派给同一模型连接,继承当前工作区和可用能力;长输出可分页续读。',
access: 'read',
group: 'programming'
}
@@ -139,21 +139,21 @@ export const builtinModelToolGroups = [
id: 'filesystem',
name: '工作区文件',
description:
'搜索和分页读取工作区文件;Execute 模式还可通过补丁新增、修改或删除文件。',
'搜索和分页读取工作区文件,通过补丁新增、修改或删除文件。',
tools: builtinModelTools.filter((tool) => tool.group === 'filesystem')
},
{
id: 'web',
name: '联网搜索',
description:
'启用后,直连模型可在 Ask 和 Execute 模式搜索并读取公开网页。',
'启用后,直连模型可搜索并读取公开网页。',
tools: builtinModelTools.filter((tool) => tool.group === 'web')
},
{
id: 'programming',
name: '开发工具',
description:
'直连模型可运行项目命令,或将聚焦的开发任务委派给继承当前模式的 Subagent。',
'直连模型可运行项目命令,或将聚焦的开发任务委派给使用相同工作区和可用能力的 Subagent。',
tools: builtinModelTools.filter((tool) => tool.group === 'programming')
}
] as const
-1
View File
@@ -358,7 +358,6 @@ export const webSearchCapabilitySchema = z
.object({
provider: z.literal('exa'),
enabled: z.boolean(),
availableIn: z.tuple([z.literal('ask'), z.literal('execute')]),
tools: z.tuple([
z.literal('web_search'),
z.literal('web_fetch')
-3
View File
@@ -19,8 +19,6 @@ const channelIdentifierSchema = z
.min(1)
.max(CHANNEL_LIMITS.maximumIdentityLength)
export const channelWorkModeSchema = z.literal('ask')
export type ChannelWorkMode = z.infer<typeof channelWorkModeSchema>
const attachmentBase64Schema = z
.string()
@@ -126,7 +124,6 @@ export const channelInboundTextSchema = z
.max(CHANNEL_LIMITS.maximumErrorLength)
.optional(),
mentioned: z.boolean().default(false),
workMode: channelWorkModeSchema.default('ask'),
receivedAt: z.number().int().nonnegative().optional()
})
.strict()
+32 -1
View File
@@ -9,9 +9,14 @@ import {
browserStopLoadingRequestSchema,
builtinEmbeddingConnectionId,
clipboardTextSchema,
conversationQueueUserInputSchema,
legacyEmbeddingConnectionId,
runtimeSettingsInputSchema
} from './contracts'
import { projectCreateSchema, scheduleCreateSchema } from './assistant-contracts'
import { channelInboundTextSchema } from './channel-contracts'
import { runtimeNativeToolSchema } from './runtime-customization-contracts'
import { webSearchCapabilitySchema } from './capability-contracts'
describe('clipboard text contract', () => {
it('accepts text and rejects non-text input', () => {
@@ -167,9 +172,35 @@ const baseInput = {
workspacePath: 'workspace',
apiKey: { action: 'keep' as const },
deepseekHarnessModelSource: { kind: 'platform' as const },
toolApproval: 'always' as const
}
describe('unified execution contracts', () => {
it('accepts mode-free requests without adding a default mode', () => {
const request = { requestId: crypto.randomUUID(), conversationId: 'conversation', prompt: 'hello' }
expect(agentRequestSchema.parse(request)).not.toHaveProperty('workMode')
expect(conversationQueueUserInputSchema.parse({ conversationId: 'conversation', prompt: 'hello' })).not.toHaveProperty('workMode')
expect(projectCreateSchema.parse({ name: 'Project', description: '', rootPath: '/workspace' })).not.toHaveProperty('defaultWorkMode')
expect(scheduleCreateSchema.parse({ title: 'Task', prompt: 'hello', recurrence: 'once', nextRunAt: '2030-01-01T00:00:00Z' })).not.toHaveProperty('workMode')
expect(channelInboundTextSchema.parse({ channel: 'wecom', eventId: 'event', senderId: 'sender',
conversationId: 'conversation', conversationType: 'direct', text: 'hello' })).not.toHaveProperty('workMode')
expect(() => agentRequestSchema.parse({ ...request, workMode: undefined })).toThrow()
})
it('describes registered tools without mode availability fields', () => {
const native = { id: 'read', name: 'Read', description: 'Reads files', kind: 'read', source: 'runtime' }
expect(runtimeNativeToolSchema.parse(native)).toEqual(native)
expect(() => runtimeNativeToolSchema.parse({ ...native, ask: undefined, execute: undefined })).toThrow()
const web = { provider: 'exa', enabled: true, tools: ['web_search', 'web_fetch'] }
expect(webSearchCapabilitySchema.parse(web)).toEqual(web)
expect(() => webSearchCapabilitySchema.parse({ ...web, availableIn: undefined })).toThrow()
})
it('accepts settings without a tool policy and rejects the removed setting', () => {
expect(runtimeSettingsInputSchema.parse(baseInput)).not.toHaveProperty('toolApproval')
expect(() => runtimeSettingsInputSchema.parse({ ...baseInput, toolApproval: undefined })).toThrow()
})
})
describe('embedding connection settings input', () => {
it('accepts additive builtin and user connection updates', () => {
expect(
+2 -18
View File
@@ -32,7 +32,6 @@ import {
conversationContextCompressionStateSchema,
actorConversationSubagentActivitySchema,
legacyConversationSubagentActivitySchema,
legacyWorkModeSchema,
type AssistantProject,
type AssistantArtifact,
type AssistantMemory,
@@ -352,7 +351,6 @@ export const agentRequestSchema = z
queueItemId: z.string().uuid().optional(),
runtimeSelection: optionalAgentRuntimeSelectionSchema,
runtimeControl: runtimeControlSchema.optional(),
workMode: legacyWorkModeSchema.optional(),
prompt: z.string().trim().min(1).max(100_000),
knowledgeLibraryIds: z
.array(z.string().uuid())
@@ -399,7 +397,6 @@ export const conversationQueueUserInputSchema = z
expertId: z.string().uuid().optional(),
teamMode: z.boolean().optional(),
smartRouting: z.boolean().optional(),
workMode: legacyWorkModeSchema,
includeMemoryContext: z.boolean().default(true),
prompt: z.string().trim().min(1).max(100_000),
attachments: z.array(conversationAttachmentSchema).max(maximumAttachmentsPerMessage).default([]),
@@ -434,13 +431,6 @@ export const runtimeProviderSchema = z.enum([
'deepseek-harness'
])
export const toolApprovalPolicySchema = z.enum([
'always',
'session',
'workspace',
'policy'
])
export const continueModeSchema = z.enum(['chat', 'agent'])
export const modelProtocolSchema = z.enum([
'anthropic-messages',
@@ -549,8 +539,7 @@ export const defaultRuntimeSettings = {
knowledgeRerankModel: 'rerank-v3.5',
contextCompression: defaultContextCompressionSettings,
runtimeCustomization: defaultRuntimeCustomizationSettings,
workspacePath: '',
toolApproval: 'always'
workspacePath: ''
} as const
export const runtimePathSchema = z
@@ -787,7 +776,6 @@ export const runtimeSettingsInputSchema = z
continueModelSource: runtimeModelSourceSchema.optional(),
deepseekHarnessModelSource: runtimeModelSourceSchema
.default({ kind: 'platform' }),
toolApproval: toolApprovalPolicySchema
}).strict()
.superRefine((settings, context) => {
if (
@@ -1153,7 +1141,6 @@ export type RuntimeSettings = {
| { source: 'profile'; profileId: string; name: string; modelName: string }
| { source: 'unavailable' }
>
toolApproval: RuntimeSettingsInput['toolApproval']
configured?: ConfiguredRuntimeSettings
warnings?: SettingsWarning[]
}
@@ -1808,10 +1795,6 @@ export type DesktopApi = {
) => Promise<AgentRuntimeStatus>
run: (request: AgentRequest) => Promise<void>
cancel: (requestId: string) => Promise<void>
respondApproval: (
approvalId: string,
decision: ApprovalDecision
) => Promise<void>
respondQuestion: (
questionId: string,
answers?: AgentQuestionAnswer[]
@@ -2134,6 +2117,7 @@ export type DesktopApi = {
) => () => void
}
workspace: {
importFiles: (projectId: string, path: string) => Promise<{ imported: string[]; failed: { name: string; error: string }[] }>
manage: (projectId: string, action: import('./workspace-management-contracts').WorkspaceManagementAction) => Promise<import('./workspace-management-contracts').WorkspaceManagementResult>
getChanges: (projectId: string) => Promise<WorkspaceChanges>
getFileDiff: (projectId: string, path: string) => Promise<WorkspaceChanges>
+2 -2
View File
@@ -55,8 +55,8 @@ export const goodbuddyConfigToolCatalog = {
name: 'goodbuddy_config_apply',
title: 'Apply a GoodBuddy configuration plan',
description:
'Apply a previously planned request-scoped change in Execute mode. The plan must be unexpired and can be applied only once. Raw operations and secrets are not accepted. If a later operation fails, the result reports partial application and the remaining operations are not attempted.',
summary: 'Apply one request-scoped configuration plan in Execute mode.',
'Apply a previously planned request-scoped change. The plan must be unexpired and can be applied only once. Raw operations and secrets are not accepted. If a later operation fails, the result reports partial application and the remaining operations are not attempted.',
summary: 'Apply one request-scoped configuration plan.',
access: 'write',
inputSchema: goodbuddyConfigApplyInputSchema,
outputSchema: goodbuddyConfigApplyOutputSchema
+3 -4
View File
@@ -37,12 +37,11 @@ export type ImageRequestContext = {
conversationId: string
messageId: string
requestId: string
workMode: 'ask' | 'execute'
}
export const imageSaveToolInputSchema = z.object({
artifactId: z.string().uuid(),
path: z.string().trim().min(1).max(4_096),
artifactId: z.string().uuid().describe('Image ID from the conversation image/upload references or generate_image results. No results page is required; never invent IDs.'),
path: z.string().trim().min(1).max(4_096).describe('Absolute destination file path on the runtime host (the remote host for remote projects). Writes the actual image file.'),
overwrite: z.boolean().default(false)
}).strict()
export type ImageSaveToolInput = z.infer<typeof imageSaveToolInputSchema>
@@ -69,7 +68,7 @@ export function imageSaveMimeTypeForPath(path: string): ImageSaveResult['mimeTyp
export const imageSaveMaximumBytes = 48 * 1024 * 1024
export const imageSaveToolName = 'save_image' as const
export const imageSaveToolDescription = 'Save an existing conversation image (generated result or uploaded source) to a local file. Use an artifactId listed in this conversation; never invent IDs. path must be absolute and end with .png, .jpg, .jpeg or .webp; the image is converted when the extension differs from its stored format (conversion to .webp is not supported). Missing parent folders are created. Existing files are kept unless overwrite=true. The file is written on the machine where this tool runs (the remote host for remote projects).'
export const imageSaveToolDescription = 'Write an existing conversation image to an actual file on the runtime host (the remote host for remote projects). artifactId is the image ID from conversation image/upload references or generate_image results; no results page is required. Never invent IDs. path must be absolute and end with .png, .jpg, .jpeg or .webp; the image is converted when the extension differs from its stored format (conversion to .webp is not supported). Missing parent folders are created. Existing files are kept unless overwrite=true.'
export const imageToolName = 'generate_image' as const
export const imageToolDescriptionLimit = 16_000
+1 -1
View File
@@ -41,7 +41,6 @@ export const ipcChannels = {
agentStatus: 'agent:get-status',
agentRun: 'agent:run',
agentCancel: 'agent:cancel',
agentApprovalRespond: 'agent:approval:respond',
agentQuestionRespond: 'agent:question:respond',
agentCompactConversation: 'agent:context:compact',
agentEvent: 'agent:event',
@@ -266,6 +265,7 @@ export const ipcChannels = {
artifactsList: 'artifacts:list',
artifactsGet: 'artifacts:get',
artifactsImportFiles: 'artifacts:import-files',
workspaceImportFiles: 'workspace:import-files',
memoryList: 'memory:list',
memoryCreate: 'memory:create',
memorySetStatus: 'memory:set-status',
+6 -8
View File
@@ -77,7 +77,7 @@ describe('remote Agent contracts', () => {
}
})
it('uses workMode as the complete prompt authorization contract', () => {
it('uses a mode-free prompt contract and rejects legacy mode fields', () => {
const common = {
bindingId: 'binding-1',
operationId: 'operation-1',
@@ -100,11 +100,9 @@ describe('remote Agent contracts', () => {
maximumOutputBytes: 100
}
}
const execute = remotePromptOperationPreparationSchema.parse({
...common,
workMode: 'execute'
})
expect(execute.workMode).toBe('execute')
const execute = remotePromptOperationPreparationSchema.parse(common)
expect(execute).not.toHaveProperty('workMode')
expect(() => remotePromptOperationPreparationSchema.parse({ ...common, workMode: 'execute' })).toThrow()
expect(execute).not.toHaveProperty('trustTier')
expect(execute).not.toHaveProperty('capabilities')
expect(execute.budget).not.toHaveProperty('maximumToolCalls')
@@ -112,7 +110,6 @@ describe('remote Agent contracts', () => {
bindingId: execute.bindingId,
operationId: execute.operationId,
requestId: execute.requestId,
workMode: execute.workMode,
deadlineAt: execute.deadlineAt,
acceptedAt: '2029-01-01T00:00:00.000Z'
})
@@ -122,9 +119,10 @@ describe('remote Agent contracts', () => {
expect(() =>
assertRemotePromptAcceptanceMatchesPreparation(execute, {
...acceptance,
workMode: 'ask'
operationId: 'another-operation'
})
).toThrow(/does not match/iu)
expect(() => remotePromptOperationAcceptanceSchema.parse({ ...acceptance, workMode: 'ask' })).toThrow()
})
it('rejects delivered and ACK cursors ahead of durable journals', () => {
+1 -3
View File
@@ -150,7 +150,6 @@ const remotePromptOperationIdentityFields = {
bindingId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
workMode: z.enum(['ask', 'execute']),
controllerId: agentIdentifierSchema,
controllerGeneration: z.number().int().min(1).max(0xffff_ffff),
connectionGeneration: z.number().int().min(1).max(0xffff_ffff),
@@ -356,7 +355,6 @@ export const remotePromptOperationAcceptanceSchema = z
bindingId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
workMode: z.enum(['ask', 'execute']),
deadlineAt: z.string().datetime({ offset: true }),
acceptedAt: z.string().datetime({ offset: true })
})
@@ -386,7 +384,6 @@ export function assertRemotePromptAcceptanceMatchesPreparation(
acceptance.bindingId !== preparation.bindingId ||
acceptance.operationId !== preparation.operationId ||
acceptance.requestId !== preparation.requestId ||
acceptance.workMode !== preparation.workMode ||
acceptance.deadlineAt !== preparation.deadlineAt
) {
throw new Error('Runtime prompt acceptance identity does not match')
@@ -669,6 +666,7 @@ export const remoteWorkspaceEntrySchema = z
kind: remoteWorkspaceEntryKindSchema,
byteLength: z.number().int().min(0).safe().optional(),
modifiedAt: z.string().datetime({ offset: true }).optional(),
createdAt: z.string().datetime({ offset: true }).optional(),
digest: sha256DigestSchema.optional(),
executable: z.boolean()
})
@@ -13,7 +13,6 @@ const profileId = '00000000-0000-4000-8000-000000000040'
const draft = {
name: '远程项目',
description: 'Awaited save',
defaultWorkMode: 'ask' as const,
runtimeSelection: {
provider: 'opencode' as const,
model: { kind: 'profile' as const, profileId }
@@ -51,18 +50,12 @@ describe('remote project save contracts', () => {
).toThrow()
})
it('accepts Execute without consent or trust fields', () => {
expect(
remoteProjectCreateDraftSchema.parse({
...draft,
defaultWorkMode: 'execute'
}).defaultWorkMode
).toBe('execute')
it('accepts a project without consent or trust fields', () => {
expect(remoteProjectCreateDraftSchema.parse(draft)).toEqual(draft)
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
trust: {},
defaultWorkMode: 'execute'
trust: {}
})
).toThrow()
})
@@ -29,7 +29,6 @@ export const remoteProjectRootPathSchema = remoteAbsolutePathSchema
const commonDraftFields = {
name: projectCreateSchema.shape.name,
description: projectCreateSchema.shape.description,
defaultWorkMode: projectCreateSchema.shape.defaultWorkMode,
runtimeSelection: runtimeSelectionLayerSchema.optional(),
hostId: sshHostIdSchema,
remoteRootPath: remoteProjectRootPathSchema
@@ -85,7 +85,6 @@ const signedBundle = {
const startPayload = {
bindingId: 'binding-1',
requestId: 'request-1',
workMode: 'ask' as const,
runtimeId: 'runtime-1',
adapterParameters: [
{ name: 'modelProfileId' as const, value: 'model-1' }
@@ -362,7 +362,6 @@ export const remoteRuntimeStartPayloadSchema = z
.object({
bindingId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
workMode: z.enum(['ask', 'execute']),
runtimeId: agentIdentifierSchema,
adapterParameters: remoteRuntimeAdapterParametersSchema,
deadlineAt: remoteRuntimeDeadlineSchema,
@@ -374,9 +374,7 @@ export const runtimeNativeToolSchema = z
'mcp',
'skill',
'unknown'
]),
ask: z.enum(['allowed', 'blocked', 'conditional']),
execute: z.enum(['allowed', 'blocked', 'conditional'])
])
})
.strict()
+5 -1
View File
@@ -13,6 +13,11 @@ const terminalId = '00000000-0000-4000-8000-000000000102'
const projectId = '00000000-0000-4000-8000-000000000201'
describe('workbar contracts', () => {
it('drops only retired results instances from released layouts', () => {
const browser = { id: terminalId, appId: 'browser', title: 'Research', targetRef: { type: 'conversation', conversationId: 'A' } }
const restored = normalizeWorkbarLayoutPreferences({ instances: [{ id: taskId, appId: 'results', title: 'Results' }, browser], activeInstanceId: taskId, expanded: false, dock: 'right', widthRatio: 0.42 }, [])
expect(restored).toMatchObject({ instances: [browser], activeInstanceId: terminalId, expanded: false, widthRatio: 0.42 })
})
it('keeps notes as a context-independent single instance through layout normalization', () => {
const instance = { id: taskId, appId: 'notes', title: 'Notes' } as const
const layout = { instances: [instance, { ...instance, id: terminalId }], activeInstanceId: terminalId, expanded: true, dock: 'right', widthRatio: 0.3 }
@@ -43,7 +48,6 @@ describe('workbar contracts', () => {
{ id: 'tasks', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: true, closable: false, reorderable: false },
{ id: 'workspace', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: true, closable: false, reorderable: false },
{ id: 'browser', instancePolicy: 'multiple', defaultContext: 'current-conversation', defaultOpen: true, required: false, closable: true, reorderable: true },
{ id: 'results', instancePolicy: 'single', defaultContext: 'current-project', defaultOpen: true, required: false, closable: true, reorderable: true },
{ id: 'notes', instancePolicy: 'single', defaultContext: 'application', defaultOpen: false, required: false, closable: true, reorderable: true },
{
id: 'terminal',
+5 -15
View File
@@ -23,7 +23,6 @@ export const workbarAppIdSchema = z.enum([
'tasks',
'workspace',
'browser',
'results',
'notes',
'terminal'
])
@@ -139,20 +138,6 @@ export const WORKBAR_APP_DEFINITIONS = [
reorderable: true,
availability: { state: 'available' }
},
{
id: 'results',
visibleAcrossContextSwitches: true,
label: '成果',
icon: 'results',
description: '查看任务生成的成果。',
instancePolicy: 'single',
defaultContext: 'current-project',
defaultOpen: true,
required: false,
closable: true,
reorderable: true,
availability: { state: 'available' }
},
{
id: 'notes',
visibleAcrossContextSwitches: true,
@@ -357,6 +342,11 @@ export function normalizeWorkbarLayoutPreferences(
value: unknown,
defaultInstances: readonly WorkbarTabInstance[]
): WorkbarLayoutPreferences | undefined {
// Released layouts may contain the retired results tab; retain all other preferences.
if (value && typeof value === 'object' && 'instances' in value && Array.isArray(value.instances)) {
value = { ...value, instances: value.instances.filter((instance: unknown) =>
!instance || typeof instance !== 'object' || !('appId' in instance) || instance.appId !== 'results') }
}
const parsed = workbarLayoutShapeSchema.safeParse(value)
if (!parsed.success) {
return undefined
@@ -8,6 +8,7 @@ const branchSchema = z.string().min(1).max(1024).refine((value) => !value.starts
const oidSchema = z.string().regex(/^[a-f0-9]{40,64}$/)
export const workspaceManagementActionSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('importFile'), path: pathSchema, offset: z.number().int().nonnegative().safe(), data: z.string().max(349528).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/) }).strict(),
z.object({ kind: z.literal('createFile'), path: pathSchema }).strict(),
z.object({ kind: z.literal('createDirectory'), path: pathSchema }).strict(),
z.object({ kind: z.literal('move'), path: pathSchema, destination: pathSchema }).strict(),