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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ui/src/features/agent/agentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`)
}
Expand Down
28 changes: 27 additions & 1 deletion ui/src/features/agent/applicationAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +124,10 @@ export interface QueueAdapter {
resume(taskId: string, confirm: boolean): Promise<AdapterOutcome>
retry(taskId: string, confirm: boolean): Promise<AdapterOutcome>
}
export interface WorkspaceAdapter {
select(action: AgentSelectWorkspaceAction): Promise<AdapterOutcome>
create(action: AgentCreateWorkspaceAction): Promise<AdapterOutcome>
}

export interface Video3DAdapter {
open(animate?: boolean): Promise<AdapterOutcome>
Expand All @@ -141,6 +145,7 @@ export interface WizardApplicationAdapters {
videoEditor: VideoEditorAdapter
characterKit: CharacterKitAdapter
queue: QueueAdapter
workspace: WorkspaceAdapter
openTab(tab: AgentTab): Promise<AdapterOutcome>
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -808,6 +823,17 @@ async function presentQueueSliceResult(result: CommandResult): Promise<AdapterOu
}
}

async function presentWorkspaceSliceResult(result: CommandResult): Promise<AdapterOutcome> {
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<AdapterOutcome & { state: 'completed' | 'partial' | 'failed' }> {
await navigate('comics')
const meta = result.artifacts[0]?.metadata || {}
Expand Down
18 changes: 4 additions & 14 deletions ui/src/features/agent/navigationQueueCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ import {
openAgentSeriesSection,
openAgentStorySection,
} from './agentUiBus'
import {
createAgentWorkspace,
selectAgentWorkspace,
} from './workspaceActions'

/**
* The registry owns the concrete implementation of defineCapability. Keeping
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down
41 changes: 35 additions & 6 deletions ui/src/features/workspaces/actions.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
export async function selectAgentWorkspace(requestedName: string): Promise<CommandResult> {
if (requestedName === '__uploads__') {
throw new Error('Uploads es una vista virtual de sólo lectura, no un workspace seleccionable para generar.')
}
Expand All @@ -19,30 +39,39 @@ export async function selectAgentWorkspace(requestedName: string): Promise<strin
throw new Error(`No existe el workspace “${requestedName}”. Los disponibles son: ${before.workspaces.map(item => 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<string> {
export async function createAgentWorkspace(requestedName: string): Promise<CommandResult> {
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()
const created = after.workspaces.find(item => normalized(item.name) === normalized(name))
if (!created || after.active !== created.name || useStore.getState().activeWorkspace !== created.name) {
throw new Error(`El backend no confirmó la creación y selección de “${name}”.`)
}
return `He creado y seleccionado el workspace “${created.name}”. El chat continúa aquí y las nuevas generaciones se guardarán en él.`
return workspaceResult(
created.name,
`He creado y seleccionado el workspace “${created.name}”. El chat continúa aquí y las nuevas generaciones se guardarán en él.`,
)
}
4 changes: 2 additions & 2 deletions ui/src/features/workspaces/adapters.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createAgentWorkspace, selectAgentWorkspace } from './actions'
import type { CreateWorkspaceCommand, SelectWorkspaceCommand } from './commands'

export async function selectWorkspace(command: SelectWorkspaceCommand): Promise<string> {
export async function selectWorkspace(command: SelectWorkspaceCommand) {
return selectAgentWorkspace(command.workspaceName)
}

export async function createWorkspace(command: CreateWorkspaceCommand): Promise<string> {
export async function createWorkspace(command: CreateWorkspaceCommand) {
return createAgentWorkspace(command.workspaceName)
}
5 changes: 1 addition & 4 deletions ui/tests/agentCapabilityPorts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ const LEGACY_EXECUTE_ALLOWLIST = [
'start_generation',
'attach_studio_references',
'configure_studio_loras',
'select_workspace',
'create_workspace',
]

const AGENT_ACTIONS_IMPORTS = [
Expand All @@ -67,7 +65,6 @@ const AGENT_ACTIONS_IMPORTS = [
'./studioGuidance',
'./videoEditorActions',
'./wizardContext',
'./workspaceActions',
]

const LAB_ACTIONS_IMPORTS = [
Expand Down Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion ui/tests/agentContract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
34 changes: 34 additions & 0 deletions ui/tests/navigationQueueCapabilities.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']])
})