diff --git a/ui/src/features/agent/agentActions.ts b/ui/src/features/agent/agentActions.ts index c1441a49..64cc0d02 100644 --- a/ui/src/features/agent/agentActions.ts +++ b/ui/src/features/agent/agentActions.ts @@ -3219,11 +3219,11 @@ export async function executeAgentActions( const outcome = await defaultApplicationAdapters.queue.retry(action.taskId, action.confirm) results.push({ action, ok: true, message: outcome.message, report: outcome.report }) } else if (action.type === 'select_workspace') { - const { selectAgentWorkspace } = await import('./workspaceActions') - results.push({ action, ok: true, message: await selectAgentWorkspace(action.workspaceName) }) + const outcome = await defaultApplicationAdapters.workspace.select(action) + results.push({ action, ok: true, message: outcome.message, report: outcome.report }) } else if (action.type === 'create_workspace') { - const { createAgentWorkspace } = await import('./workspaceActions') - results.push({ action, ok: true, message: await createAgentWorkspace(action.workspaceName) }) + const outcome = await defaultApplicationAdapters.workspace.create(action) + results.push({ action, ok: true, message: outcome.message, report: outcome.report }) } else { throw new Error(`No hay ejecutor para ${action.type}.`) } diff --git a/ui/src/features/agent/applicationAdapters.ts b/ui/src/features/agent/applicationAdapters.ts index 2f9febe5..87851319 100644 --- a/ui/src/features/agent/applicationAdapters.ts +++ b/ui/src/features/agent/applicationAdapters.ts @@ -4,7 +4,7 @@ import { rememberedCharacterKitLibrary } from '../characters/session' import type { SeriesAssemblyJob } from '../series/assemblyContract' import type { SeriesJobStatus } from '../series/types' import type { MediaFilter } from '../../types' -import type { AgentApply3dRhythmAction, AgentApplySeriesPlanAction, AgentApplyStoryProposalAction, AgentApproveStorySectionAction, AgentApproveStoryVisualsAction, AgentAssembleSeriesEpisodeAction, AgentCommitSeriesCanonAction, AgentConfigureStorySongAction, AgentCreateComicAction, AgentCreateSeriesEpisodeAction, AgentCreateStoryAction, AgentGenerateComicAction, AgentGenerateSeriesPlanAction, AgentGenerateStorySectionAction, AgentGenerateStorySongAction, AgentGenerateStoryVisualsAction, AgentRenderSeriesShotsAction, AgentReviewSeriesAttemptsAction, AgentStageStoryComicAction, AgentStartDirectorProductionAction, AgentStageStoryMusicVideoAction, AgentStageStoryVideoAction, AgentUpdateSeriesEpisodeAction, AgentUpdateStoryAction } from './agentActions' +import type { AgentApply3dRhythmAction, AgentApplySeriesPlanAction, AgentApplyStoryProposalAction, AgentApproveStorySectionAction, AgentApproveStoryVisualsAction, AgentAssembleSeriesEpisodeAction, AgentCommitSeriesCanonAction, AgentConfigureStorySongAction, AgentCreateComicAction, AgentCreateSeriesEpisodeAction, AgentCreateStoryAction, AgentCreateWorkspaceAction, AgentGenerateComicAction, AgentGenerateSeriesPlanAction, AgentGenerateStorySectionAction, AgentGenerateStorySongAction, AgentGenerateStoryVisualsAction, AgentRenderSeriesShotsAction, AgentReviewSeriesAttemptsAction, AgentSelectWorkspaceAction, AgentStageStoryComicAction, AgentStartDirectorProductionAction, AgentStageStoryMusicVideoAction, AgentStageStoryVideoAction, AgentUpdateSeriesEpisodeAction, AgentUpdateStoryAction } from './agentActions' import { executionKey, executionReport, @@ -124,6 +124,10 @@ export interface QueueAdapter { resume(taskId: string, confirm: boolean): Promise retry(taskId: string, confirm: boolean): Promise } +export interface WorkspaceAdapter { + select(action: AgentSelectWorkspaceAction): Promise + create(action: AgentCreateWorkspaceAction): Promise +} export interface Video3DAdapter { open(animate?: boolean): Promise @@ -141,6 +145,7 @@ export interface WizardApplicationAdapters { videoEditor: VideoEditorAdapter characterKit: CharacterKitAdapter queue: QueueAdapter + workspace: WorkspaceAdapter openTab(tab: AgentTab): Promise } @@ -668,6 +673,16 @@ export function createDefaultApplicationAdapters(): WizardApplicationAdapters { return presentQueueSliceResult(await retry({ taskId, confirm: true })) }, } + adapters.workspace = { + async select(action) { + const { selectWorkspace } = await import('../workspaces/adapters') + return presentWorkspaceSliceResult(await selectWorkspace({ workspaceName: action.workspaceName })) + }, + async create(action) { + const { createWorkspace } = await import('../workspaces/adapters') + return presentWorkspaceSliceResult(await createWorkspace({ workspaceName: action.workspaceName })) + }, + } adapters.video3d = { open: animate => navigate(animate ? 'animate_3d' : 'video_3d'), async applyRhythm(action) { @@ -808,6 +823,17 @@ async function presentQueueSliceResult(result: CommandResult): Promise { + const summary = typeof result.artifacts[0]?.metadata?.summary === 'string' + ? result.artifacts[0].metadata.summary + : 'Workspace listo.' + const name = String(result.artifacts[0]?.metadata?.title || result.entities[0]?.id || 'workspace') + return { + message: summary, + target: { kind: 'workspace', id: result.entities[0]?.id || name, title: name }, + } +} + async function presentComicSliceResult(result: CommandResult): Promise { await navigate('comics') const meta = result.artifacts[0]?.metadata || {} diff --git a/ui/src/features/agent/navigationQueueCapabilities.ts b/ui/src/features/agent/navigationQueueCapabilities.ts index d3f087a3..3e1bc033 100644 --- a/ui/src/features/agent/navigationQueueCapabilities.ts +++ b/ui/src/features/agent/navigationQueueCapabilities.ts @@ -18,10 +18,6 @@ import { openAgentSeriesSection, openAgentStorySection, } from './agentUiBus' -import { - createAgentWorkspace, - selectAgentWorkspace, -} from './workspaceActions' /** * The registry owns the concrete implementation of defineCapability. Keeping @@ -350,11 +346,8 @@ export function registerNavigationQueueCapabilities( resolve(raw) { return workspaceName('select_workspace', raw) }, validate(action) { return action.workspaceName.trim() ? [] : ['workspace name is required'] }, async prepare(action) { return action }, - async execute(action) { - return { - message: await selectAgentWorkspace(action.workspaceName), - target: { kind: 'workspace', id: action.workspaceName, title: action.workspaceName }, - } + async execute(action, context) { + return context.adapters.workspace.select(action) }, correlate(_action, outcome) { return outcome.target }, async track(_action, outcome) { return outcome }, @@ -384,11 +377,8 @@ export function registerNavigationQueueCapabilities( resolve(raw) { return workspaceName('create_workspace', raw) }, validate(action) { return action.workspaceName.trim() ? [] : ['workspace name is required'] }, async prepare(action) { return action }, - async execute(action) { - return { - message: await createAgentWorkspace(action.workspaceName), - target: { kind: 'workspace', id: action.workspaceName, title: action.workspaceName }, - } + async execute(action, context) { + return context.adapters.workspace.create(action) }, correlate(_action, outcome) { return outcome.target }, async track(_action, outcome) { return outcome }, diff --git a/ui/src/features/workspaces/actions.ts b/ui/src/features/workspaces/actions.ts index c964ad26..cccd6fcc 100644 --- a/ui/src/features/workspaces/actions.ts +++ b/ui/src/features/workspaces/actions.ts @@ -1,15 +1,35 @@ +import { commandResultFromSlice, type CommandResult } from '../../lib/commandContract' import * as api from '../../api/client' import { useStore } from '../../stores/useStore' const normalized = (value: string): string => value.trim().toLocaleLowerCase() +function workspaceResult(name: string, message: string): CommandResult { + const entity = { kind: 'workspace', id: name, workspaceId: name } + return commandResultFromSlice({ + entity, + artifacts: [{ + id: 'reply', + kind: 'document', + owner: entity, + uri: 'workspace:reply', + metadata: { summary: message, title: name }, + }], + }) +} + +function summaryOf(result: CommandResult): string { + const summary = result.artifacts[0]?.metadata?.summary + return typeof summary === 'string' ? summary : 'Workspace listo.' +} + async function authoritativeWorkspaces() { const result = await api.fetchWorkspaces() useStore.setState({ workspaces: result.workspaces }) return result } -export async function selectAgentWorkspace(requestedName: string): Promise { +export async function selectAgentWorkspace(requestedName: string): Promise { if (requestedName === '__uploads__') { throw new Error('Uploads es una vista virtual de sólo lectura, no un workspace seleccionable para generar.') } @@ -19,24 +39,30 @@ export async function selectAgentWorkspace(requestedName: string): Promise item.name).join(', ') || 'ninguno'}.`) } if (before.active === workspace.name && useStore.getState().activeWorkspace === workspace.name) { - return `El workspace “${workspace.name}” ya estaba activo.` + return workspaceResult(workspace.name, `El workspace “${workspace.name}” ya estaba activo.`) } await useStore.getState().switchWorkspace(workspace.name) const after = await api.fetchWorkspaces() if (after.active !== workspace.name || useStore.getState().activeWorkspace !== workspace.name) { throw new Error(`El backend no confirmó el cambio al workspace “${workspace.name}”; no afirmaré que se completó.`) } - return `He cambiado al workspace “${workspace.name}”. El chat y las siguientes acciones continúan en ese contexto.` + return workspaceResult( + workspace.name, + `He cambiado al workspace “${workspace.name}”. El chat y las siguientes acciones continúan en ese contexto.`, + ) } -export async function createAgentWorkspace(requestedName: string): Promise { +export async function createAgentWorkspace(requestedName: string): Promise { const name = requestedName.trim() if (!name || name === '__uploads__') throw new Error('Ese nombre de workspace no es válido.') const before = await authoritativeWorkspaces() const existing = before.workspaces.find(item => normalized(item.name) === normalized(name)) if (existing) { const selected = await selectAgentWorkspace(existing.name) - return `El workspace “${existing.name}” ya existía. ${selected}` + return workspaceResult( + existing.name, + `El workspace “${existing.name}” ya existía. ${summaryOf(selected)}`, + ) } await useStore.getState().createWorkspace(name) const after = await api.fetchWorkspaces() @@ -44,5 +70,8 @@ export async function createAgentWorkspace(requestedName: string): Promise { +export async function selectWorkspace(command: SelectWorkspaceCommand) { return selectAgentWorkspace(command.workspaceName) } -export async function createWorkspace(command: CreateWorkspaceCommand): Promise { +export async function createWorkspace(command: CreateWorkspaceCommand) { return createAgentWorkspace(command.workspaceName) } diff --git a/ui/tests/agentCapabilityPorts.test.mjs b/ui/tests/agentCapabilityPorts.test.mjs index 5c00b5cb..6a4dc2b0 100644 --- a/ui/tests/agentCapabilityPorts.test.mjs +++ b/ui/tests/agentCapabilityPorts.test.mjs @@ -42,8 +42,6 @@ const LEGACY_EXECUTE_ALLOWLIST = [ 'start_generation', 'attach_studio_references', 'configure_studio_loras', - 'select_workspace', - 'create_workspace', ] const AGENT_ACTIONS_IMPORTS = [ @@ -67,7 +65,6 @@ const AGENT_ACTIONS_IMPORTS = [ './studioGuidance', './videoEditorActions', './wizardContext', - './workspaceActions', ] const LAB_ACTIONS_IMPORTS = [ @@ -216,7 +213,7 @@ test('capabilities execute through adapters except the frozen legacy executors', + `added=${JSON.stringify(added)} removed=${JSON.stringify(removed)}`, ) assert.equal(registered.length, 73) - assert.equal(legacy.length, 12) + assert.equal(legacy.length, 10) }) test('agentActions.ts and labActions.ts keep their current module graph until a slice PR shrinks it', () => { diff --git a/ui/tests/agentContract.test.mjs b/ui/tests/agentContract.test.mjs index b7c71701..5032e808 100644 --- a/ui/tests/agentContract.test.mjs +++ b/ui/tests/agentContract.test.mjs @@ -165,7 +165,7 @@ test('application adapters navigate and verify targets without rendering React', try { const adapters = createDefaultApplicationAdapters() assert.deepEqual(Object.keys(adapters).sort(), [ - 'characterKit', 'comic', 'openTab', 'queue', 'seriesLab', 'storyLab', 'studio', 'video3d', 'videoEditor', + 'characterKit', 'comic', 'openTab', 'queue', 'seriesLab', 'storyLab', 'studio', 'video3d', 'videoEditor', 'workspace', ]) const story = await adapters.storyLab.open() assert.equal(useStore.getState().mediaFilter, 'stories') diff --git a/ui/tests/navigationQueueCapabilities.test.mjs b/ui/tests/navigationQueueCapabilities.test.mjs index 1da8a4a5..8ad5e3f2 100644 --- a/ui/tests/navigationQueueCapabilities.test.mjs +++ b/ui/tests/navigationQueueCapabilities.test.mjs @@ -132,3 +132,37 @@ test('section capabilities retain the visible lab navigation effect', async () = window.removeEventListener('hocuspocus:story-section', onStory) window.removeEventListener('hocuspocus:series-section', onSeries) }) + +test('workspace capabilities execute through adapters instead of Agent Mode helpers', async () => { + const { registerNavigationQueueCapabilities } = await import('../src/features/agent/navigationQueueCapabilities.ts') + const definitions = new Map() + registerNavigationQueueCapabilities(definition => { + definitions.set(definition.name, definition) + return definition + }) + const seen = [] + const context = { + adapters: { + workspace: { + async select(action) { + seen.push(['select', action.workspaceName]) + return { message: `He cambiado a “${action.workspaceName}”.`, target: { kind: 'workspace', id: action.workspaceName, title: action.workspaceName } } + }, + async create(action) { + seen.push(['create', action.workspaceName]) + return { message: `He creado “${action.workspaceName}”.`, target: { kind: 'workspace', id: action.workspaceName, title: action.workspaceName } } + }, + }, + }, + } + + const selected = await definitions.get('select_workspace').execute( + { type: 'select_workspace', workspaceName: 'Faro' }, context, + ) + const created = await definitions.get('create_workspace').execute( + { type: 'create_workspace', workspaceName: 'Nuevo taller' }, context, + ) + assert.equal(selected.message, 'He cambiado a “Faro”.') + assert.equal(created.message, 'He creado “Nuevo taller”.') + assert.deepEqual(seen, [['select', 'Faro'], ['create', 'Nuevo taller']]) +})