mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
Display automation host and support cross-authority moves
Users can now move automations to different storage authorities. The destination picker shows all available hosts, and selecting a new one displays a warning about the move. The save creates the automation on the destination and deletes it from the source; if deletion fails, both copies remain and the user is notified.
This commit is contained in:
@@ -61,10 +61,12 @@ const LEGACY_ENTRY: AutomationHostCatalogEntry = {
|
||||
|
||||
function control(
|
||||
projects: Repo[],
|
||||
entries: AutomationHostCatalogEntry[] = [ENTRY]
|
||||
entries: AutomationHostCatalogEntry[] = [ENTRY],
|
||||
note?: string
|
||||
): AutomationCreateDestinationControl {
|
||||
return {
|
||||
entries,
|
||||
note,
|
||||
resolution: {
|
||||
status: 'ready',
|
||||
authority: OWNER.authority,
|
||||
@@ -76,11 +78,11 @@ function control(
|
||||
}
|
||||
}
|
||||
|
||||
function render(projects: Repo[], entries?: AutomationHostCatalogEntry[]): void {
|
||||
function render(projects: Repo[], entries?: AutomationHostCatalogEntry[], note?: string): void {
|
||||
act(() => {
|
||||
root.render(
|
||||
<TooltipProvider>
|
||||
<AutomationDestinationField control={control(projects, entries)} />
|
||||
<AutomationDestinationField control={control(projects, entries, note)} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
@@ -117,6 +119,16 @@ describe('AutomationDestinationField', () => {
|
||||
expect(container.querySelector('[data-testid="automation-create-update-required"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('states a move in place of the host it would otherwise just name', () => {
|
||||
render([{ id: 'repo-1' } as Repo], undefined, 'Saving creates this automation on GPU box.')
|
||||
|
||||
// Both lines name the same host; the one that mentions the delete wins.
|
||||
expect(container.querySelector('[data-testid="automation-host-move"]')?.textContent).toContain(
|
||||
'GPU box'
|
||||
)
|
||||
expect(container.textContent).not.toContain('Stored and scheduled by')
|
||||
})
|
||||
|
||||
it('names the host without a tense, so editing reads as a move', () => {
|
||||
render([{ id: 'repo-1' } as Repo])
|
||||
|
||||
|
||||
@@ -19,18 +19,9 @@ import {
|
||||
import { groupAutomationHostEntriesByAuthority } from './automation-host-picker-groups'
|
||||
import type { AutomationCreateDestinationControl } from './use-automation-create-destination'
|
||||
|
||||
/**
|
||||
* Which host stores and schedules the automation, named on the form before submit.
|
||||
*
|
||||
* This is the storage authority, not the workspace's execution host — the two
|
||||
* routinely differ, and only this one decides which machine keeps and schedules
|
||||
* the record. A concrete host filter only preselects: the list filter narrows a
|
||||
* view, so it must not decide where a new record is allowed to live.
|
||||
*
|
||||
* Editing offers the same picker over the record's own authority, because a save
|
||||
* that lands on another host is a move, not a create. The copy stays tense-free
|
||||
* so both readings hold.
|
||||
*/
|
||||
// The host that stores and schedules the automation, not the workspace's
|
||||
// execution host — the two routinely differ. Serves create and edit alike; on an
|
||||
// existing record a save that lands elsewhere is a move.
|
||||
|
||||
export function AutomationDestinationField({
|
||||
control,
|
||||
@@ -88,6 +79,12 @@ export function AutomationDestinationField({
|
||||
'No projects are set up on {host}. Add one there, or choose another host.'
|
||||
).replace('{host}', selected.label)}
|
||||
</p>
|
||||
) : control.note ? (
|
||||
// Replaces the storedOn line: both name the same host, and the move is
|
||||
// the consequential half.
|
||||
<p className="text-xs text-destructive" data-testid="automation-host-move">
|
||||
{control.note}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selected
|
||||
|
||||
+79
-6
@@ -414,7 +414,7 @@ describe('AutomationsPage edit dialog projects', () => {
|
||||
})
|
||||
|
||||
describe('AutomationsPage edit destination', () => {
|
||||
it('names the edited row’s own host, and only its authority’s hosts', async () => {
|
||||
it('names the edited row’s own host, and offers every host it could move to', async () => {
|
||||
const automation = makeAutomation({ id: 'a-1' })
|
||||
api.automations.list.mockResolvedValue([])
|
||||
scopedList([automation])
|
||||
@@ -430,11 +430,84 @@ describe('AutomationsPage edit destination', () => {
|
||||
|
||||
const resolution = mocks.editorDialog?.editDestination?.resolution
|
||||
expect(resolution?.status === 'ready' && resolution.entry.stableKey).toBe(DESKTOP_SELF_KEY)
|
||||
// A record cannot move between authorities, so the runtime is not offered.
|
||||
expect(mocks.editorDialog?.editDestination?.entries.map((entry) => entry.stableKey)).toEqual([
|
||||
DESKTOP_SELF_KEY,
|
||||
SSH_HOST_KEY
|
||||
])
|
||||
// Another authority is a legal destination — reached by a create there and a
|
||||
// delete here — so restricting the list to this one would hide a real move.
|
||||
expect(mocks.editorDialog?.editDestination?.entries.map((entry) => entry.stableKey)).toContain(
|
||||
RUNTIME_SELF_KEY
|
||||
)
|
||||
expect(mocks.editorDialog?.editDestination?.note).toBeFalsy()
|
||||
})
|
||||
|
||||
it('moves the record across authorities by creating there and deleting here', async () => {
|
||||
const automation = makeAutomation({ id: 'a-1' })
|
||||
api.automations.list.mockResolvedValue([])
|
||||
scopedList([automation])
|
||||
runtimeHost([], [])
|
||||
addRuntimeProject()
|
||||
runtimeCreateReturns(makeAutomation({ id: 'a-moved', projectId: RUNTIME_REPO_ID }))
|
||||
|
||||
await renderPage()
|
||||
await settleHostQueries()
|
||||
await act(async () => {
|
||||
void mocks.listPanel?.openEditDialog(listedRow(automation.id))
|
||||
})
|
||||
await act(async () => {
|
||||
mocks.editorDialog?.editDestination?.onSelect(RUNTIME_SELF_KEY)
|
||||
})
|
||||
|
||||
// The move is stated before it happens: the record is deleted from here.
|
||||
expect(mocks.editorDialog?.editDestination?.note).toBeTruthy()
|
||||
|
||||
await act(async () => {
|
||||
mocks.editorDialog?.onDraftChange((current) => ({
|
||||
...(current as Record<string, unknown>),
|
||||
projectId: RUNTIME_REPO_ID,
|
||||
workspaceMode: 'existing',
|
||||
workspaceId: RUNTIME_WORKSPACE_ID
|
||||
}))
|
||||
})
|
||||
await save()
|
||||
|
||||
// `update` reaches only the authority already holding the record, so a move
|
||||
// is the pair — and leaving the original behind would run it twice.
|
||||
expect(api.automations.update).not.toHaveBeenCalled()
|
||||
expect(runtimeCreateCalls()).toHaveLength(1)
|
||||
expect(runtimeCreateCalls()[0]?.[2]).toMatchObject({ repo: `id:${RUNTIME_REPO_ID}` })
|
||||
expect(api.automations.delete).toHaveBeenCalledWith(expect.objectContaining({ id: 'a-1' }))
|
||||
})
|
||||
|
||||
it('reports a kept original as two live copies, never as a completed move', async () => {
|
||||
const automation = makeAutomation({ id: 'a-1' })
|
||||
api.automations.list.mockResolvedValue([])
|
||||
scopedList([automation])
|
||||
runtimeHost([], [])
|
||||
addRuntimeProject()
|
||||
runtimeCreateReturns(makeAutomation({ id: 'a-moved', projectId: RUNTIME_REPO_ID }))
|
||||
api.automations.delete.mockRejectedValue(new Error('gone away'))
|
||||
|
||||
await renderPage()
|
||||
await settleHostQueries()
|
||||
await act(async () => {
|
||||
void mocks.listPanel?.openEditDialog(listedRow(automation.id))
|
||||
})
|
||||
await act(async () => {
|
||||
mocks.editorDialog?.editDestination?.onSelect(RUNTIME_SELF_KEY)
|
||||
})
|
||||
await act(async () => {
|
||||
mocks.editorDialog?.onDraftChange((current) => ({
|
||||
...(current as Record<string, unknown>),
|
||||
projectId: RUNTIME_REPO_ID,
|
||||
workspaceMode: 'existing',
|
||||
workspaceId: RUNTIME_WORKSPACE_ID
|
||||
}))
|
||||
})
|
||||
await save()
|
||||
|
||||
// The new copy is live and the old one still runs; "moved" would hide that.
|
||||
expect(runtimeCreateCalls()).toHaveLength(1)
|
||||
expect(api.automations.delete).toHaveBeenCalled()
|
||||
expect(mocks.toastError).toHaveBeenCalledWith(expect.stringContaining('could not be deleted'))
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('moves the record to the host the user picks, clearing the stranded project', async () => {
|
||||
|
||||
+18
@@ -53,6 +53,24 @@ function runtimeAutomationCalls(): string[] {
|
||||
}
|
||||
|
||||
describe('AutomationsPage row actions under a colliding automation id', () => {
|
||||
it('names the selected row’s own host, not the desktop that listed it', async () => {
|
||||
await collidingHosts()
|
||||
await renderPage()
|
||||
await settleHostQueries()
|
||||
|
||||
const remote = mocks.listPanel?.filteredRows.find(
|
||||
(candidate) => candidate.automation.name === 'Remote nightly'
|
||||
)
|
||||
await act(async () => {
|
||||
mocks.listPanel?.selectAutomationRow(remote?.key ?? '')
|
||||
})
|
||||
|
||||
// The runtime copy's own execution target reads `local` — local to that
|
||||
// server — so only the row's catalog entry can name the host on the detail view.
|
||||
expect(mocks.detailPane?.selected?.name).toBe('Remote nightly')
|
||||
expect(mocks.detailPane?.selectedHostEntry?.authorityLabel).toBe('GPU box')
|
||||
})
|
||||
|
||||
it('runs the selected desktop row on the desktop, not on the runtime holding the same id', async () => {
|
||||
await collidingHosts()
|
||||
const { container } = await renderPage()
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
import {
|
||||
deleteAutomationForTarget,
|
||||
type AutomationHostTarget,
|
||||
getAutomationAuthorityTarget,
|
||||
getAutomationHostTargetFromKey,
|
||||
getAutomationHostTargetKey,
|
||||
getAutomationListTarget,
|
||||
@@ -1024,25 +1025,36 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
})()
|
||||
const isOrcaForm = createTarget === 'orca' && editingExternalTarget === null
|
||||
const dialogAuthorityRepos = getAutomationCreateRepos(repos, automationDialogTarget)
|
||||
// An update moves the record's execution target, never the authority that
|
||||
// stores it, so the edit picker offers only that authority's own hosts.
|
||||
// The authority storing the edited record; a save landing on another one is a move.
|
||||
const dialogAuthorityKey = automationAuthorityCatalogKey(
|
||||
automationDialogTarget.kind === 'environment'
|
||||
? { kind: 'runtime', environmentId: automationDialogTarget.environmentId }
|
||||
: { kind: 'desktop' }
|
||||
)
|
||||
const editHostEntries = hostCatalog.entries.filter(
|
||||
(entry) => automationAuthorityCatalogKey(entry.stableRef.authority) === dialogAuthorityKey
|
||||
)
|
||||
const editHostEntries = hostCatalog.entries
|
||||
const editHostResolution = resolveAutomationCreateDestination(
|
||||
editingHostStableKey
|
||||
? editHostEntries.find((entry) => entry.stableKey === editingHostStableKey)
|
||||
: null
|
||||
)
|
||||
// Projects follow the picked host's authority, not the record's: a move offers
|
||||
// what the target stores, and nothing on the old authority is reachable there.
|
||||
const editHostProjects =
|
||||
editHostResolution.status === 'ready'
|
||||
? automationCreateEligibleProjects(repoTables, editHostResolution, dialogAuthorityRepos)
|
||||
? automationCreateEligibleProjects(
|
||||
repoTables,
|
||||
editHostResolution,
|
||||
getAutomationCreateRepos(
|
||||
repos,
|
||||
getAutomationAuthorityTarget(editHostResolution.authority)
|
||||
)
|
||||
)
|
||||
: dialogAuthorityRepos
|
||||
const editMoveTargetEntry =
|
||||
editHostResolution.status === 'ready' &&
|
||||
automationAuthorityCatalogKey(editHostResolution.authority) !== dialogAuthorityKey
|
||||
? editHostResolution.entry
|
||||
: null
|
||||
// Picking a host strands a project that lives on another one; clearing it here
|
||||
// states the move instead of deferring the same refusal to submit.
|
||||
const handleEditHostChange = (stableKey: string): void => {
|
||||
@@ -1065,7 +1077,14 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
entries: editHostEntries,
|
||||
resolution: editHostResolution,
|
||||
onSelect: handleEditHostChange,
|
||||
projects: editHostProjects
|
||||
projects: editHostProjects,
|
||||
// Replacer fn: a literal replacement would expand `$` patterns in host labels.
|
||||
note: editMoveTargetEntry
|
||||
? translate(
|
||||
'auto.components.automations.createDestination.move',
|
||||
'Saving creates this automation on {host} and deletes the original. Its run history stays on the old host.'
|
||||
).replace('{host}', () => editMoveTargetEntry.authorityLabel)
|
||||
: null
|
||||
}
|
||||
const dialogRepos = isOrcaForm
|
||||
? editingAutomationId !== null
|
||||
@@ -1732,6 +1751,7 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
currentAutomation = (reread.ok ? reread.value : null) ?? currentAutomation
|
||||
}
|
||||
let editDestination: AutomationDestination | undefined
|
||||
let moveTarget: AutomationCreateDestination | null = null
|
||||
const destinationChanged =
|
||||
currentAutomation &&
|
||||
(currentAutomation.projectId !== draft.projectId ||
|
||||
@@ -1776,6 +1796,11 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
return
|
||||
}
|
||||
editDestination = revalidated.destination
|
||||
// `update` reaches only the authority already holding the record, so a
|
||||
// save landing on another one is a create there plus a delete here.
|
||||
if (automationAuthorityCatalogKey(revalidated.authority) !== dialogAuthorityKey) {
|
||||
moveTarget = revalidated
|
||||
}
|
||||
}
|
||||
const updates: AutomationUpdateInput = {
|
||||
name: draft.name,
|
||||
@@ -1797,42 +1822,53 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
updates.rrule = rrule
|
||||
updates.dtstart = now
|
||||
}
|
||||
const createInput: AutomationCreateInput = {
|
||||
name: draft.name,
|
||||
prompt: draft.prompt,
|
||||
precheck,
|
||||
agentId: draft.agentId,
|
||||
runContext,
|
||||
projectId: draft.projectId,
|
||||
workspaceMode: draft.workspaceMode,
|
||||
workspaceId: draft.workspaceId,
|
||||
baseBranch: draft.baseBranch.trim() || null,
|
||||
setupDecision,
|
||||
reuseSession: draft.workspaceMode === 'existing' && draft.reuseSession,
|
||||
timezone,
|
||||
rrule,
|
||||
dtstart: updates.dtstart ?? currentAutomation?.dtstart ?? now,
|
||||
missedRunGraceMinutes
|
||||
}
|
||||
const editSource = currentAutomation
|
||||
const saved = editingAutomationId
|
||||
? await dispatchAutomationUpdate(
|
||||
automationDispatchContext,
|
||||
{ rowKey: editingRowKey ?? '', automationId: editingAutomationId },
|
||||
updates,
|
||||
() => {
|
||||
// Nothing names a host: no captured owner and no record to read one
|
||||
// from, so the save is refused rather than sent unfenced.
|
||||
if (!editSource) {
|
||||
throw new Error(
|
||||
automationOwnerConflictMessage(AUTOMATION_OWNER_CONFLICT_CODES.ownerChanged)
|
||||
)
|
||||
}
|
||||
return updateAutomationForTarget(editSource, updates, automationHostTarget)
|
||||
},
|
||||
'save',
|
||||
editDestination
|
||||
)
|
||||
: await createDraftAutomation({
|
||||
name: draft.name,
|
||||
prompt: draft.prompt,
|
||||
precheck,
|
||||
agentId: draft.agentId,
|
||||
runContext,
|
||||
projectId: draft.projectId,
|
||||
workspaceMode: draft.workspaceMode,
|
||||
workspaceId: draft.workspaceId,
|
||||
baseBranch: draft.baseBranch.trim() || null,
|
||||
setupDecision,
|
||||
reuseSession: draft.workspaceMode === 'existing' && draft.reuseSession,
|
||||
timezone,
|
||||
rrule,
|
||||
dtstart: now,
|
||||
missedRunGraceMinutes
|
||||
const move = moveTarget
|
||||
? await moveAutomationToDestination(editSource, moveTarget, {
|
||||
...createInput,
|
||||
// A move carries the record across; it does not resume a paused one.
|
||||
enabled: editSource?.enabled ?? true,
|
||||
sourceContext: editSource?.sourceContext ?? null
|
||||
})
|
||||
: null
|
||||
const saved = move
|
||||
? move.saved
|
||||
: editingAutomationId
|
||||
? await dispatchAutomationUpdate(
|
||||
automationDispatchContext,
|
||||
{ rowKey: editingRowKey ?? '', automationId: editingAutomationId },
|
||||
updates,
|
||||
() => {
|
||||
// Nothing names a host: no captured owner and no record to read one
|
||||
// from, so the save is refused rather than sent unfenced.
|
||||
if (!editSource) {
|
||||
throw new Error(
|
||||
automationOwnerConflictMessage(AUTOMATION_OWNER_CONFLICT_CODES.ownerChanged)
|
||||
)
|
||||
}
|
||||
return updateAutomationForTarget(editSource, updates, automationHostTarget)
|
||||
},
|
||||
'save',
|
||||
editDestination
|
||||
)
|
||||
: await createDraftAutomation(createInput)
|
||||
if (!saved.ok) {
|
||||
setEditorNotice(saved.notice)
|
||||
return
|
||||
@@ -1851,7 +1887,9 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
})
|
||||
setDraft((current) => ({ ...current, name: '', prompt: '' }))
|
||||
await refresh()
|
||||
if (editingAutomationId && editingRowKey) {
|
||||
// A move leaves the edited row behind: the record it selects is a new one
|
||||
// on another host, so its key is found rather than restored.
|
||||
if (editingAutomationId && editingRowKey && !moveTarget) {
|
||||
setSelectedAutomationRunPageId(null)
|
||||
setSelectedRowKey(editingRowKey)
|
||||
setSelectedId(automation.id)
|
||||
@@ -1862,13 +1900,25 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
if (!editingAutomationId) {
|
||||
useAppStore.getState().recordFeatureInteraction('automation-created')
|
||||
}
|
||||
// A kept original already raised its own toast; "moved" would contradict it.
|
||||
if (move && !move.originalRemoved) {
|
||||
return
|
||||
}
|
||||
toast.success(
|
||||
editingAutomationId
|
||||
moveTarget
|
||||
? translate(
|
||||
'auto.components.automations.AutomationsPage.244727e655',
|
||||
'Automation updated.'
|
||||
)
|
||||
: translate('auto.components.automations.AutomationsPage.2a20596d6b', 'Automation saved.')
|
||||
'auto.components.automations.AutomationsPage.moved',
|
||||
'Automation moved to {host}.'
|
||||
).replace('{host}', () => moveTarget.entry.authorityLabel)
|
||||
: editingAutomationId
|
||||
? translate(
|
||||
'auto.components.automations.AutomationsPage.244727e655',
|
||||
'Automation updated.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.automations.AutomationsPage.2a20596d6b',
|
||||
'Automation saved.'
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
if (isHermesSave) {
|
||||
@@ -1887,6 +1937,55 @@ export default function AutomationsPage(): React.JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A record cannot change the authority that stores it, so a save aimed at
|
||||
* another one creates the automation there and deletes the original. A failed
|
||||
* delete leaves two live copies, which is said rather than reported as a move.
|
||||
*/
|
||||
const moveAutomationToDestination = async (
|
||||
source: Automation | null,
|
||||
target: AutomationCreateDestination,
|
||||
input: AutomationCreateInput
|
||||
): Promise<{ saved: AutomationDispatchResult<Automation>; originalRemoved: boolean }> => {
|
||||
if (!source) {
|
||||
return {
|
||||
saved: {
|
||||
ok: false,
|
||||
notice: {
|
||||
message: automationOwnerConflictMessage(AUTOMATION_OWNER_CONFLICT_CODES.ownerChanged),
|
||||
recovery: 'retry',
|
||||
severity: 'owner'
|
||||
}
|
||||
},
|
||||
originalRemoved: false
|
||||
}
|
||||
}
|
||||
const created = toDispatchResult(
|
||||
await createAutomationAtDestination(target.authority, input, target.destination)
|
||||
)
|
||||
if (!created.ok) {
|
||||
return { saved: created, originalRemoved: false }
|
||||
}
|
||||
invalidateWrittenHost(target.entry.stableRef, 'definition')
|
||||
const removed = await dispatchAutomationDelete(
|
||||
automationDispatchContext,
|
||||
{ rowKey: editingRowKey ?? '', automationId: source.id },
|
||||
// The dialog's own target, which names the edited row's host even when no
|
||||
// owner was captured to fence the delete with.
|
||||
() => deleteAutomationForTarget(source, automationDialogTarget)
|
||||
)
|
||||
if (!removed.ok) {
|
||||
// Both copies are live; said as such rather than reported as a move.
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.automations.AutomationsPage.moveOriginalKept',
|
||||
'Created on {host}, but the original could not be deleted. Remove it on the old host.'
|
||||
).replace('{host}', () => target.entry.authorityLabel)
|
||||
)
|
||||
}
|
||||
return { saved: created, originalRemoved: removed.ok }
|
||||
}
|
||||
|
||||
/** Creation states its destination and re-checks it at submit; it never infers one. */
|
||||
const createDraftAutomation = async (
|
||||
input: AutomationCreateInput
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
AutomationRun,
|
||||
AutomationUpdateInput
|
||||
} from '../../../../shared/automations-types'
|
||||
import type { AutomationAuthorityRef } from '../../../../shared/automation-owner-ref'
|
||||
import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
|
||||
@@ -49,6 +50,14 @@ export function getAutomationTargetFromHostId(
|
||||
: { kind: 'local' }
|
||||
}
|
||||
|
||||
export function getAutomationAuthorityTarget(
|
||||
authority: AutomationAuthorityRef
|
||||
): AutomationHostTarget {
|
||||
return authority.kind === 'runtime'
|
||||
? { kind: 'environment', environmentId: authority.environmentId }
|
||||
: { kind: 'local' }
|
||||
}
|
||||
|
||||
export function getAutomationListTarget(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
|
||||
): AutomationHostTarget {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
/**
|
||||
* The host a stored automation belongs to, named on the detail view.
|
||||
*
|
||||
* The catalog entry the row was listed from is the truthful source: it names the
|
||||
* storing authority too, which the record's own execution target cannot — a
|
||||
* runtime-stored automation's target reads `local`, meaning local to that
|
||||
* server, not to this Mac. The record's target is only the fallback for a legacy
|
||||
* row no host answered for.
|
||||
*/
|
||||
// The row's catalog entry names the storing authority; the record's own target
|
||||
// cannot — a runtime-stored automation reads `local`, meaning local to that
|
||||
// server. That target is only the fallback for a legacy row no host answered for.
|
||||
|
||||
import type { Automation } from '../../../../shared/automations-types'
|
||||
import { getExecutionHostLabel, toSshExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY
|
||||
} from '../../../../shared/protocol-version'
|
||||
import type { AppState } from '@/store'
|
||||
import type { AutomationHostCatalogEntry } from './automation-host-catalog-types'
|
||||
import type { AutomationHostCatalogView } from './use-automation-host-catalog'
|
||||
import type { AutomationCreateDestinationControl } from './use-automation-create-destination'
|
||||
import type { ExternalAutomationListEntry } from './external-automation-list-entries'
|
||||
@@ -76,6 +77,7 @@ export type ListPanelProps = {
|
||||
|
||||
export type DetailPaneProps = {
|
||||
selected: Automation | null
|
||||
selectedHostEntry: AutomationHostCatalogEntry | null
|
||||
selectedRuns: AutomationRun[]
|
||||
selectedRunsNotice: { message: string } | null
|
||||
runNow: (automation: Automation) => void
|
||||
|
||||
@@ -35,6 +35,8 @@ export type AutomationCreateDestinationControl = {
|
||||
onSelect: (stableKey: string) => void
|
||||
/** Projects the resolved destination can hold; the whole list until one resolves. */
|
||||
projects: readonly Repo[]
|
||||
/** Stated consequence of saving here, when it is not the plain one. */
|
||||
note?: string | null
|
||||
}
|
||||
|
||||
export type AutomationCreateDestinationCheck =
|
||||
|
||||
@@ -15376,6 +15376,8 @@
|
||||
"b11170a008": "Failed to save automation.",
|
||||
"2a20596d6b": "Automation saved.",
|
||||
"244727e655": "Automation updated.",
|
||||
"moved": "Automation moved to {host}.",
|
||||
"moveOriginalKept": "Created on {host}, but the original could not be deleted. Remove it on the old host.",
|
||||
"77b81bc4ac": "Hermes automation created.",
|
||||
"08efc3ae12": "Hermes automation updated.",
|
||||
"e431bb85d4": "Choose a workspace on the same host as this Hermes automation.",
|
||||
@@ -15690,7 +15692,8 @@
|
||||
"stale": "{host} changed while this form was open. Choose it again before saving.",
|
||||
"projectMismatch": "That project is not on {host}. Choose a project on that host, or another host.",
|
||||
"noProjects": "No projects are set up on {host}. Add one there, or choose another host.",
|
||||
"updateRequired": "Update the Orca server on {hosts} to store automations there."
|
||||
"updateRequired": "Update the Orca server on {hosts} to store automations there.",
|
||||
"move": "Saving creates this automation on {host} and deletes the original. Its run history stays on the old host."
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
|
||||
@@ -13724,6 +13724,7 @@
|
||||
"showMore": "Mostrar más"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"host": "Host",
|
||||
"007c8ad874": "Inmediato",
|
||||
"a1d52c2189": "Cobertura de uso",
|
||||
"449fc83bf7": "Fichas",
|
||||
@@ -13887,6 +13888,8 @@
|
||||
"b11170a008": "No se pudo guardar la automatización.",
|
||||
"2a20596d6b": "Automatización guardada.",
|
||||
"244727e655": "Automatización actualizada.",
|
||||
"moved": "Automatización movida a {host}.",
|
||||
"moveOriginalKept": "Se creó en {host}, pero no se pudo eliminar la original. Elimínela en el host anterior.",
|
||||
"77b81bc4ac": "Se crea la automatización de Hermes.",
|
||||
"08efc3ae12": "Automatización Hermes actualizada.",
|
||||
"e431bb85d4": "Elija un espacio de trabajo en el mismo host que esta automatización de Hermes.",
|
||||
@@ -14083,6 +14086,19 @@
|
||||
"filters": "Filtros",
|
||||
"clear": "Limpiar filtros",
|
||||
"removeFilter": "Eliminar filtro de {{value0}}"
|
||||
},
|
||||
"createDestination": {
|
||||
"label": "Host",
|
||||
"placeholder": "Seleccionar un host",
|
||||
"storedOn": "Almacenada y programada por {authority}.",
|
||||
"unselected": "Elija el host que almacena y programa esta automatización.",
|
||||
"orphan": "Las automatizaciones sin host no pueden alojar otras nuevas. Elija un host en el que crear esta automatización.",
|
||||
"unavailable": "Ese host todavía no puede alojar una nueva automatización. Elija otro host para crear esta.",
|
||||
"stale": "{host} cambió mientras este formulario estaba abierto. Elíjalo de nuevo antes de guardar.",
|
||||
"projectMismatch": "Ese proyecto no está en {host}. Elija un proyecto en ese host, u otro host.",
|
||||
"noProjects": "No hay proyectos configurados en {host}. Agregue uno allí o elija otro host.",
|
||||
"updateRequired": "Actualice el servidor de Orca en {hosts} para almacenar automatizaciones allí.",
|
||||
"move": "Al guardar se crea esta automatización en {host} y se elimina la original. Su historial de ejecuciones permanece en el host anterior."
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
|
||||
@@ -13715,6 +13715,7 @@
|
||||
"showMore": "さらに表示"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"host": "ホスト",
|
||||
"007c8ad874": "プロンプト",
|
||||
"a1d52c2189": "使用範囲",
|
||||
"449fc83bf7": "トークン",
|
||||
@@ -13878,6 +13879,8 @@
|
||||
"b11170a008": "オートメーションを保存できませんでした。",
|
||||
"2a20596d6b": "オートメーションが保存されました。",
|
||||
"244727e655": "自動化が更新されました。",
|
||||
"moved": "自動化を {host} に移動しました。",
|
||||
"moveOriginalKept": "{host} に作成しましたが、元の自動化を削除できませんでした。元のホストで削除してください。",
|
||||
"77b81bc4ac": "Hermes オートメーションが作成されました。",
|
||||
"08efc3ae12": "Hermes オートメーションが更新されました。",
|
||||
"e431bb85d4": "この Hermes オートメーションと同じホスト上のワークスペースを選択します。",
|
||||
@@ -14074,6 +14077,19 @@
|
||||
"filters": "フィルター",
|
||||
"clear": "フィルターをクリア",
|
||||
"removeFilter": "{{value0}} フィルターを解除"
|
||||
},
|
||||
"createDestination": {
|
||||
"label": "ホスト",
|
||||
"placeholder": "ホストを選択",
|
||||
"storedOn": "{authority} が保存してスケジュールします。",
|
||||
"unselected": "この自動化を保存してスケジュールするホストを選択します。",
|
||||
"orphan": "ホストのない自動化は新しい自動化を保持できません。この自動化を作成するホストを選択してください。",
|
||||
"unavailable": "そのホストはまだ新しい自動化を保持できません。これを作成する別のホストを選択してください。",
|
||||
"stale": "このフォームを開いている間に {host} が変更されました。保存する前にもう一度選択してください。",
|
||||
"projectMismatch": "そのプロジェクトは {host} にありません。そのホスト上のプロジェクト、または別のホストを選択してください。",
|
||||
"noProjects": "{host} にはプロジェクトが設定されていません。そこに追加するか、別のホストを選択してください。",
|
||||
"updateRequired": "{hosts} の Orca サーバーを更新すると、そこに自動化を保存できます。",
|
||||
"move": "保存すると、この自動化は {host} に作成され、元の自動化は削除されます。実行履歴は元のホストに残ります。"
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
|
||||
@@ -3879,17 +3879,82 @@
|
||||
},
|
||||
"skills": {
|
||||
"SkillsPage": {
|
||||
"cb142070b4": "새로 고침", "a68dee6a32": "스킬 검색", "f43ad6edf3": "스킬", "ea72d6185b": "스킬을 검색하지 못했습니다", "dc4c3328ee": "파일 표시", "9963dff6d3": "설명을 찾을 수 없습니다.", "995fde8337": "스킬 파일을 표시하지 못했습니다", "4acd6d68ec": "스킬을 찾을 수 없습니다", "6a62a0168c": "일치하는 항목 없음", "08a321a984": "현재 검색 및 필터와 일치하는 스킬이 없습니다.", "cd7893fbc1": "스킬 검색 중", "35b9a724a0": "사용 가능", "c13b82793c": "설치 관리", "aee7b99cc6": "링크에서 설치", "filterProvider": "에이전트별 필터", "filterSource": "소스별 필터", "allSources": "전체", "clearFilters": "필터 지우기", "closeSkills": "스킬 닫기", "closeTooltip": "닫기 · Esc", "moreActions": "추가 작업", "sharedLinks": "공유 링크", "emptyCopy": "검색한 스킬 폴더가 비어 있습니다. 공유 번들을 설치하거나 스킬을 추가한 후 새로 고치세요.", "retry": "다시 시도", "remoteShareNotice": "이 스킬은 {{host}}에 있습니다. 공유하려면 해당 머신에서 스킬을 여세요.", "viewSwitch": "표시", "searchLinks": "링크 검색", "deleteSkills": "스킬 삭제…"
|
||||
"cb142070b4": "새로 고침",
|
||||
"a68dee6a32": "스킬 검색",
|
||||
"f43ad6edf3": "스킬",
|
||||
"ea72d6185b": "스킬을 검색하지 못했습니다",
|
||||
"dc4c3328ee": "파일 표시",
|
||||
"9963dff6d3": "설명을 찾을 수 없습니다.",
|
||||
"995fde8337": "스킬 파일을 표시하지 못했습니다",
|
||||
"4acd6d68ec": "스킬을 찾을 수 없습니다",
|
||||
"6a62a0168c": "일치하는 항목 없음",
|
||||
"08a321a984": "현재 검색 및 필터와 일치하는 스킬이 없습니다.",
|
||||
"cd7893fbc1": "스킬 검색 중",
|
||||
"35b9a724a0": "사용 가능",
|
||||
"c13b82793c": "설치 관리",
|
||||
"aee7b99cc6": "링크에서 설치",
|
||||
"filterProvider": "에이전트별 필터",
|
||||
"filterSource": "소스별 필터",
|
||||
"allSources": "전체",
|
||||
"clearFilters": "필터 지우기",
|
||||
"closeSkills": "스킬 닫기",
|
||||
"closeTooltip": "닫기 · Esc",
|
||||
"moreActions": "추가 작업",
|
||||
"sharedLinks": "공유 링크",
|
||||
"emptyCopy": "검색한 스킬 폴더가 비어 있습니다. 공유 번들을 설치하거나 스킬을 추가한 후 새로 고치세요.",
|
||||
"retry": "다시 시도",
|
||||
"remoteShareNotice": "이 스킬은 {{host}}에 있습니다. 공유하려면 해당 머신에서 스킬을 여세요.",
|
||||
"viewSwitch": "표시",
|
||||
"searchLinks": "링크 검색",
|
||||
"deleteSkills": "스킬 삭제…"
|
||||
},
|
||||
"SkillShareSelectionControls": { "01c5a15e02": "스킬 공유" },
|
||||
"SkillRow": { "updatedUnknown": "날짜 없음", "pathCopied": "경로 복사됨", "copyPath": "경로 복사", "detailPath": "경로", "skillActions": "{{value0}} 작업", "viewDetails": "세부 정보 보기", "deleteSkill": "삭제…" },
|
||||
"SkillRow": {
|
||||
"updatedUnknown": "날짜 없음",
|
||||
"pathCopied": "경로 복사됨",
|
||||
"copyPath": "경로 복사",
|
||||
"detailPath": "경로",
|
||||
"skillActions": "{{value0}} 작업",
|
||||
"viewDetails": "세부 정보 보기",
|
||||
"deleteSkill": "삭제…"
|
||||
},
|
||||
"SkillsList": { "listLabel": "스킬" },
|
||||
"sourceStatus": { "missing": "폴더를 찾을 수 없음", "remoteRepo": "원격 리포지토리 — 검색 안 됨", "unavailable": "검색 안 됨" },
|
||||
"sourceStatus": {
|
||||
"missing": "폴더를 찾을 수 없음",
|
||||
"remoteRepo": "원격 리포지토리 — 검색 안 됨",
|
||||
"unavailable": "검색 안 됨"
|
||||
},
|
||||
"sources": { "heading": "스킬 폴더" },
|
||||
"sourceKind": { "home": "홈", "workspace": "워크스페이스", "bundled": "번들", "plugin": "플러그인" },
|
||||
"count": { "skillOne": "스킬 {{count}}개", "skillOther": "스킬 {{count}}개", "sourceOne": "소스 {{count}}개", "sourceOther": "소스 {{count}}개", "fileOne": "파일 {{count}}개", "fileOther": "파일 {{count}}개", "resultOne": "결과 {{count}}개", "resultOther": "결과 {{count}}개", "selected": "{{count}}개 선택됨", "shareOne": "스킬 {{count}}개 공유", "shareOther": "스킬 {{count}}개 공유", "linkOne": "링크 {{count}}개", "linkOther": "링크 {{count}}개" },
|
||||
"sourceKind": {
|
||||
"home": "홈",
|
||||
"workspace": "워크스페이스",
|
||||
"bundled": "번들",
|
||||
"plugin": "플러그인"
|
||||
},
|
||||
"count": {
|
||||
"skillOne": "스킬 {{count}}개",
|
||||
"skillOther": "스킬 {{count}}개",
|
||||
"sourceOne": "소스 {{count}}개",
|
||||
"sourceOther": "소스 {{count}}개",
|
||||
"fileOne": "파일 {{count}}개",
|
||||
"fileOther": "파일 {{count}}개",
|
||||
"resultOne": "결과 {{count}}개",
|
||||
"resultOther": "결과 {{count}}개",
|
||||
"selected": "{{count}}개 선택됨",
|
||||
"shareOne": "스킬 {{count}}개 공유",
|
||||
"shareOther": "스킬 {{count}}개 공유",
|
||||
"linkOne": "링크 {{count}}개",
|
||||
"linkOther": "링크 {{count}}개"
|
||||
},
|
||||
"filter": { "allAgents": "모든 에이전트", "sharedAgent": "공유됨 (.agents)" },
|
||||
"SkillsSelectionHeader": { "exit": "선택 나가기", "exitTooltip": "선택 나가기 · Esc", "title": "공유할 스킬 선택", "selectAll": "가능한 {{count}}개 모두 선택", "clear": "지우기", "deleteTitle": "삭제할 스킬 선택" },
|
||||
"SkillsSelectionHeader": {
|
||||
"exit": "선택 나가기",
|
||||
"exitTooltip": "선택 나가기 · Esc",
|
||||
"title": "공유할 스킬 선택",
|
||||
"selectAll": "가능한 {{count}}개 모두 선택",
|
||||
"clear": "지우기",
|
||||
"deleteTitle": "삭제할 스킬 선택"
|
||||
},
|
||||
"SkillDetailDialog": { "agents": "에이전트", "updated": "업데이트됨", "copy": "복사" },
|
||||
"SkillFreshnessNudge": {
|
||||
"titleOne": "설치된 Orca 스킬이 오래되었습니다",
|
||||
@@ -13682,6 +13747,7 @@
|
||||
"showMore": "더 보기"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"host": "호스트",
|
||||
"007c8ad874": "프롬프트",
|
||||
"a1d52c2189": "사용 범위",
|
||||
"449fc83bf7": "토큰",
|
||||
@@ -13845,6 +13911,8 @@
|
||||
"b11170a008": "자동화를 저장하지 못했습니다.",
|
||||
"2a20596d6b": "자동화가 저장되었습니다.",
|
||||
"244727e655": "자동화가 업데이트되었습니다.",
|
||||
"moved": "자동화를 {host}(으)로 이동했습니다.",
|
||||
"moveOriginalKept": "{host}에 생성했지만 원본을 삭제하지 못했습니다. 이전 호스트에서 삭제하세요.",
|
||||
"77b81bc4ac": "Hermes 자동화가 생성되었습니다.",
|
||||
"08efc3ae12": "Hermes 자동화가 업데이트되었습니다.",
|
||||
"e431bb85d4": "이 Hermes 자동화와 동일한 호스트에서 워크스페이스를 선택하세요.",
|
||||
@@ -14041,6 +14109,19 @@
|
||||
"filters": "필터",
|
||||
"clear": "필터 지우기",
|
||||
"removeFilter": "{{value0}} 필터 제거"
|
||||
},
|
||||
"createDestination": {
|
||||
"label": "호스트",
|
||||
"placeholder": "호스트 선택",
|
||||
"storedOn": "{authority}에서 저장하고 예약합니다.",
|
||||
"unselected": "이 자동화를 저장하고 예약할 호스트를 선택하세요.",
|
||||
"orphan": "호스트가 없는 자동화는 새 자동화를 보유할 수 없습니다. 이 자동화를 만들 호스트를 선택하세요.",
|
||||
"unavailable": "해당 호스트는 아직 새 자동화를 보유할 수 없습니다. 이 자동화를 만들 다른 호스트를 선택하세요.",
|
||||
"stale": "이 양식이 열려 있는 동안 {host}이(가) 변경되었습니다. 저장하기 전에 다시 선택하세요.",
|
||||
"projectMismatch": "해당 프로젝트는 {host}에 없습니다. 그 호스트의 프로젝트를 선택하거나 다른 호스트를 선택하세요.",
|
||||
"noProjects": "{host}에 설정된 프로젝트가 없습니다. 거기에 추가하거나 다른 호스트를 선택하세요.",
|
||||
"updateRequired": "{hosts}의 Orca 서버를 업데이트하면 거기에 자동화를 저장할 수 있습니다.",
|
||||
"move": "저장하면 이 자동화가 {host}에 생성되고 원본은 삭제됩니다. 실행 기록은 이전 호스트에 남습니다."
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
|
||||
@@ -3889,17 +3889,90 @@
|
||||
},
|
||||
"skills": {
|
||||
"SkillsPage": {
|
||||
"cb142070b4": "刷新", "a68dee6a32": "搜索技能", "f43ad6edf3": "技能", "ea72d6185b": "无法扫描技能", "dc4c3328ee": "显示文件", "9963dff6d3": "未找到说明。", "995fde8337": "无法显示技能文件", "4acd6d68ec": "未找到技能", "6a62a0168c": "无匹配项", "08a321a984": "没有技能匹配当前搜索和筛选条件。", "cd7893fbc1": "正在扫描技能", "35b9a724a0": "可用", "c13b82793c": "管理安装", "aee7b99cc6": "从链接安装", "filterProvider": "按 Agent 筛选", "filterSource": "按来源筛选", "allSources": "全部", "clearFilters": "清除筛选", "closeSkills": "关闭技能", "closeTooltip": "关闭 · Esc", "moreActions": "更多操作", "sharedLinks": "共享链接", "emptyCopy": "扫描的技能文件夹为空。请安装共享包,或添加技能后刷新。", "retry": "重试", "remoteShareNotice": "这些技能位于 {{host}}。请在该机器上打开“技能”以进行共享。", "viewSwitch": "显示", "searchLinks": "搜索链接", "deleteSkills": "删除技能…"
|
||||
"cb142070b4": "刷新",
|
||||
"a68dee6a32": "搜索技能",
|
||||
"f43ad6edf3": "技能",
|
||||
"ea72d6185b": "无法扫描技能",
|
||||
"dc4c3328ee": "显示文件",
|
||||
"9963dff6d3": "未找到说明。",
|
||||
"995fde8337": "无法显示技能文件",
|
||||
"4acd6d68ec": "未找到技能",
|
||||
"6a62a0168c": "无匹配项",
|
||||
"08a321a984": "没有技能匹配当前搜索和筛选条件。",
|
||||
"cd7893fbc1": "正在扫描技能",
|
||||
"35b9a724a0": "可用",
|
||||
"c13b82793c": "管理安装",
|
||||
"aee7b99cc6": "从链接安装",
|
||||
"filterProvider": "按 Agent 筛选",
|
||||
"filterSource": "按来源筛选",
|
||||
"allSources": "全部",
|
||||
"clearFilters": "清除筛选",
|
||||
"closeSkills": "关闭技能",
|
||||
"closeTooltip": "关闭 · Esc",
|
||||
"moreActions": "更多操作",
|
||||
"sharedLinks": "共享链接",
|
||||
"emptyCopy": "扫描的技能文件夹为空。请安装共享包,或添加技能后刷新。",
|
||||
"retry": "重试",
|
||||
"remoteShareNotice": "这些技能位于 {{host}}。请在该机器上打开“技能”以进行共享。",
|
||||
"viewSwitch": "显示",
|
||||
"searchLinks": "搜索链接",
|
||||
"deleteSkills": "删除技能…"
|
||||
},
|
||||
"SkillShareSelectionControls": { "01c5a15e02": "共享技能" },
|
||||
"SkillRow": { "updatedUnknown": "无日期", "pathCopied": "路径已复制", "copyPath": "复制路径", "detailPath": "路径", "skillActions": "{{value0}} 的操作", "viewDetails": "查看详情", "deleteSkill": "删除…" },
|
||||
"SkillRow": {
|
||||
"updatedUnknown": "无日期",
|
||||
"pathCopied": "路径已复制",
|
||||
"copyPath": "复制路径",
|
||||
"detailPath": "路径",
|
||||
"skillActions": "{{value0}} 的操作",
|
||||
"viewDetails": "查看详情",
|
||||
"deleteSkill": "删除…"
|
||||
},
|
||||
"SkillsList": { "listLabel": "技能" },
|
||||
"sourceStatus": { "missing": "未找到文件夹", "remoteRepo": "远程仓库 — 未扫描", "unavailable": "未扫描" },
|
||||
"sourceStatus": {
|
||||
"missing": "未找到文件夹",
|
||||
"remoteRepo": "远程仓库 — 未扫描",
|
||||
"unavailable": "未扫描"
|
||||
},
|
||||
"sources": { "heading": "技能文件夹" },
|
||||
"sourceKind": { "home": "主目录", "workspace": "工作区", "bundled": "内置", "plugin": "插件" },
|
||||
"count": { "skillOne": "{{count}} 个技能", "skillOther": "{{count}} 个技能", "sourceOne": "{{count}} 个来源", "sourceOther": "{{count}} 个来源", "fileOne": "{{count}} 个文件", "fileOther": "{{count}} 个文件", "resultOne": "{{count}} 个结果", "resultOther": "{{count}} 个结果", "selected": "已选择 {{count}} 个", "shareOne": "共享 {{count}} 个技能", "shareOther": "共享 {{count}} 个技能", "linkOne": "{{count}} 个链接", "linkOther": "{{count}} 个链接", "deleteOne": "删除 {{count}} 个技能", "deleteOther": "删除 {{count}} 个技能", "deletedOne": "已删除 {{count}} 个技能", "deletedOther": "已删除 {{count}} 个技能", "deleteFolderOne": "{{count}} 个文件夹", "deleteFolderOther": "{{count}} 个文件夹", "deleteLinkOne": "{{count}} 个链接", "deleteLinkOther": "{{count}} 个链接" },
|
||||
"sourceKind": {
|
||||
"home": "主目录",
|
||||
"workspace": "工作区",
|
||||
"bundled": "内置",
|
||||
"plugin": "插件"
|
||||
},
|
||||
"count": {
|
||||
"skillOne": "{{count}} 个技能",
|
||||
"skillOther": "{{count}} 个技能",
|
||||
"sourceOne": "{{count}} 个来源",
|
||||
"sourceOther": "{{count}} 个来源",
|
||||
"fileOne": "{{count}} 个文件",
|
||||
"fileOther": "{{count}} 个文件",
|
||||
"resultOne": "{{count}} 个结果",
|
||||
"resultOther": "{{count}} 个结果",
|
||||
"selected": "已选择 {{count}} 个",
|
||||
"shareOne": "共享 {{count}} 个技能",
|
||||
"shareOther": "共享 {{count}} 个技能",
|
||||
"linkOne": "{{count}} 个链接",
|
||||
"linkOther": "{{count}} 个链接",
|
||||
"deleteOne": "删除 {{count}} 个技能",
|
||||
"deleteOther": "删除 {{count}} 个技能",
|
||||
"deletedOne": "已删除 {{count}} 个技能",
|
||||
"deletedOther": "已删除 {{count}} 个技能",
|
||||
"deleteFolderOne": "{{count}} 个文件夹",
|
||||
"deleteFolderOther": "{{count}} 个文件夹",
|
||||
"deleteLinkOne": "{{count}} 个链接",
|
||||
"deleteLinkOther": "{{count}} 个链接"
|
||||
},
|
||||
"filter": { "allAgents": "所有 Agent", "sharedAgent": "共享 (.agents)" },
|
||||
"SkillsSelectionHeader": { "exit": "退出选择", "exitTooltip": "退出选择 · Esc", "title": "选择要共享的技能", "selectAll": "选择全部 {{count}} 个符合条件的项目", "clear": "清除", "deleteTitle": "选择要删除的技能" },
|
||||
"SkillsSelectionHeader": {
|
||||
"exit": "退出选择",
|
||||
"exitTooltip": "退出选择 · Esc",
|
||||
"title": "选择要共享的技能",
|
||||
"selectAll": "选择全部 {{count}} 个符合条件的项目",
|
||||
"clear": "清除",
|
||||
"deleteTitle": "选择要删除的技能"
|
||||
},
|
||||
"SkillDetailDialog": { "agents": "Agent", "updated": "已更新", "copy": "复制" },
|
||||
"SkillFreshnessNudge": {
|
||||
"titleOne": "已安装的 Orca 技能已过期",
|
||||
@@ -13698,6 +13771,7 @@
|
||||
"showMore": "显示更多"
|
||||
},
|
||||
"AutomationDetail": {
|
||||
"host": "主机",
|
||||
"007c8ad874": "提示词",
|
||||
"a1d52c2189": "使用范围",
|
||||
"449fc83bf7": "token",
|
||||
@@ -13861,6 +13935,8 @@
|
||||
"b11170a008": "无法保存自动化。",
|
||||
"2a20596d6b": "自动化已保存。",
|
||||
"244727e655": "自动化已更新。",
|
||||
"moved": "自动化已移动到 {host}。",
|
||||
"moveOriginalKept": "已在 {host} 上创建,但无法删除原有自动化。请在原主机上删除它。",
|
||||
"77b81bc4ac": "Hermes 自动化创建。",
|
||||
"08efc3ae12": "Hermes 自动化已更新。",
|
||||
"e431bb85d4": "选择与此 Hermes 自动化位于同一主机上的工作区。",
|
||||
@@ -14057,6 +14133,19 @@
|
||||
"filters": "筛选器",
|
||||
"clear": "清除筛选器",
|
||||
"removeFilter": "移除 {{value0}} 筛选器"
|
||||
},
|
||||
"createDestination": {
|
||||
"label": "主机",
|
||||
"placeholder": "选择主机",
|
||||
"storedOn": "由 {authority} 存储和调度。",
|
||||
"unselected": "选择存储和调度此自动化的主机。",
|
||||
"orphan": "没有主机的自动化无法容纳新的自动化。请选择要在其上创建此自动化的主机。",
|
||||
"unavailable": "该主机尚无法容纳新的自动化。请选择另一台主机来创建它。",
|
||||
"stale": "此表单打开期间 {host} 已更改。保存前请重新选择。",
|
||||
"projectMismatch": "该项目不在 {host} 上。请选择该主机上的项目,或选择另一台主机。",
|
||||
"noProjects": "{host} 上未设置任何项目。请在该主机上添加一个,或选择另一台主机。",
|
||||
"updateRequired": "更新 {hosts} 上的 Orca 服务器即可在其中存储自动化。",
|
||||
"move": "保存后会在 {host} 上创建此自动化并删除原有自动化。运行历史仍保留在原主机上。"
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
|
||||
Reference in New Issue
Block a user