fix: explain when an agent cannot be run and drop the broken agent move

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-24 09:55:26 +02:00
co-authored by Claude Opus 5.5
parent ff42120888
commit 0f60dca4bd
5 changed files with 113 additions and 120 deletions
@@ -1,7 +1,6 @@
<script lang="ts">
import { base } from '$lib/base'
import Dropdown from '$lib/components/DropdownV2.svelte'
import type MoveDrawer from '$lib/components/MoveDrawer.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import DraftBadge from '$lib/components/DraftBadge.svelte'
import type ShareModal from '$lib/components/ShareModal.svelte'
@@ -13,7 +12,7 @@
import { isDeployable } from '$lib/utils_deployable'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
import { createEventDispatcher } from 'svelte'
import { FileUp, FolderInput, Pen, Shield, Trash } from 'lucide-svelte'
import { FileUp, Pen, Shield, Trash } from 'lucide-svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
@@ -25,7 +24,6 @@
agent: ListableResource & { canWrite: boolean }
marked: string | undefined
shareModal: ShareModal
moveDrawer: MoveDrawer
deploymentDrawer: DeployWorkspaceDrawer
deleteConfirmedCallback?: (() => void) | undefined
depth?: number
@@ -37,7 +35,6 @@
agent,
marked,
shareModal,
moveDrawer,
deploymentDrawer,
deleteConfirmedCallback = $bindable(),
depth = 0,
@@ -113,14 +110,6 @@
shareModal.openDrawer?.(path, 'resource')
}
},
{
displayName: 'Move/Rename',
icon: FolderInput,
disabled: !agent.canWrite || Boolean(agent.draft_only),
action: () => {
moveDrawer.openDrawer(path, agent.description, 'resource')
}
},
...(!agent.ws_specific &&
!agent.draft_only &&
isDeployable('resource', path, await getDeployUiSettings())
@@ -66,6 +66,8 @@
/** The deployed agent to run rather than a draft to edit: the run pane leads, and the form
* beside it only shows the configuration. Fixed for the mount's lifetime. */
view?: boolean
/** Why this reader cannot run the agent, if they cannot. Shown in place of the run pane. */
runBlockedReason?: string
}
let {
@@ -76,7 +78,8 @@
onSelectTool = undefined,
onSaved = undefined,
isNew = false,
view = false
view = false,
runBlockedReason = undefined
}: Props = $props()
/** The one module the editor edits. Standalone (no `agent` key) so `initFlowState` loads a
@@ -510,95 +513,101 @@
{/snippet}
{#snippet runPane()}
<div class="h-full min-h-0 flex flex-col">
<!-- Laid out as the script editor's preview column is: what a run takes above what it
produced, both alongside what is being edited. -->
<div class="flex-1 min-h-0 {testMode === 'chat' ? 'hidden' : ''}">
<Splitpanes horizontal class="h-full">
<Pane size={40} minSize={15}>
<div class="h-full overflow-auto">
<ModulePreview
mod={agentModule as FlowModule}
schema={flowLocalAgentSchema(schema)}
pickableProperties={stepPropPicker?.pickableProperties}
runInputKeys={AGENT_EDITOR_RUN_INPUTS}
bind:testJob
bind:testIsLoading
bind:scriptProgress
/>
</div>
</Pane>
<Pane size={60} minSize={20}>
<ModulePreviewResultViewer
lang="deno"
editor={undefined}
diffEditor={undefined}
mod={agentModule as FlowModule}
{testJob}
{testIsLoading}
{scriptProgress}
disableMock
disableHistory
/>
</Pane>
</Splitpanes>
{#if runBlockedReason}
<div class="h-full flex items-center justify-center px-8 text-center text-xs text-secondary">
{runBlockedReason}
</div>
{#if chatMounted}
<div class={testMode === 'chat' ? 'flex flex-col flex-1 min-h-0' : 'hidden'}>
{#if chatGap?.memory}
<div class="flex-1 flex flex-col items-center justify-center gap-2 px-8 text-center">
<MessageCircleOff size={48} class="text-tertiary opacity-50 mb-2" />
<p class="text-sm font-semibold text-emphasis">Chat needs managed memory</p>
<p class="text-xs text-secondary max-w-xs">
{chatGap.memoryCanTurnOn
? 'Without it, every message would be answered without the ones before it.'
: 'This agent replays a fixed list of messages. Switch its memory to managed to chat with it.'}
</p>
{#if chatGap.memoryCanTurnOn && !readOnly}
<Button
unifiedSize="sm"
variant="default"
btnClasses="bg-surface mt-2"
onClick={turnOnMemory}
>
Turn on managed memory
</Button>
{/if}
</div>
{:else if chatGap?.noStream}
<div
class="shrink-0 flex items-center gap-2 px-4 py-1.5 border-b text-2xs text-secondary"
>
<Info size={12} class="shrink-0" />
<span class="flex-1">
{chatGap.noStream === 'image'
? 'Image answers do not stream: each one shows once its run ends.'
: 'Streaming is off: each answer shows once its run ends.'}
</span>
{#if chatGap.noStream === 'off' && !readOnly}
<Button unifiedSize="2xs" variant="subtle" onClick={turnOnStreaming}>
Turn on
</Button>
{/if}
</div>
{/if}
<!-- Hidden rather than unmounted while memory is off: switching it off mid-turn must
{:else}
<div class="h-full min-h-0 flex flex-col">
<!-- Laid out as the script editor's preview column is: what a run takes above what it
produced, both alongside what is being edited. -->
<div class="flex-1 min-h-0 {testMode === 'chat' ? 'hidden' : ''}">
<Splitpanes horizontal class="h-full">
<Pane size={40} minSize={15}>
<div class="h-full overflow-auto">
<ModulePreview
mod={agentModule as FlowModule}
schema={flowLocalAgentSchema(schema)}
pickableProperties={stepPropPicker?.pickableProperties}
runInputKeys={AGENT_EDITOR_RUN_INPUTS}
bind:testJob
bind:testIsLoading
bind:scriptProgress
/>
</div>
</Pane>
<Pane size={60} minSize={20}>
<ModulePreviewResultViewer
lang="deno"
editor={undefined}
diffEditor={undefined}
mod={agentModule as FlowModule}
{testJob}
{testIsLoading}
{scriptProgress}
disableMock
disableHistory
/>
</Pane>
</Splitpanes>
</div>
{#if chatMounted}
<div class={testMode === 'chat' ? 'flex flex-col flex-1 min-h-0' : 'hidden'}>
{#if chatGap?.memory}
<div class="flex-1 flex flex-col items-center justify-center gap-2 px-8 text-center">
<MessageCircleOff size={48} class="text-tertiary opacity-50 mb-2" />
<p class="text-sm font-semibold text-emphasis">Chat needs managed memory</p>
<p class="text-xs text-secondary max-w-xs">
{chatGap.memoryCanTurnOn
? 'Without it, every message would be answered without the ones before it.'
: 'This agent replays a fixed list of messages. Switch its memory to managed to chat with it.'}
</p>
{#if chatGap.memoryCanTurnOn && !readOnly}
<Button
unifiedSize="sm"
variant="default"
btnClasses="bg-surface mt-2"
onClick={turnOnMemory}
>
Turn on managed memory
</Button>
{/if}
</div>
{:else if chatGap?.noStream}
<div
class="shrink-0 flex items-center gap-2 px-4 py-1.5 border-b text-2xs text-secondary"
>
<Info size={12} class="shrink-0" />
<span class="flex-1">
{chatGap.noStream === 'image'
? 'Image answers do not stream: each one shows once its run ends.'
: 'Streaming is off: each answer shows once its run ends.'}
</span>
{#if chatGap.noStream === 'off' && !readOnly}
<Button unifiedSize="2xs" variant="subtle" onClick={turnOnStreaming}>
Turn on
</Button>
{/if}
</div>
{/if}
<!-- Hidden rather than unmounted while memory is off: switching it off mid-turn must
not end the chat following that turn. Test chats, since what runs is the agent as
edited. -->
<div class={chatGap?.memory ? 'hidden' : 'flex flex-col flex-1 min-h-0'}>
<FlowChat
onRunFlow={runChatTurn}
path={chatPath}
conversationKind="test"
subject="agent"
frame="none"
inputSchema={AGENT_CHAT_SCHEMA}
flowModules={chatModules}
/>
<div class={chatGap?.memory ? 'hidden' : 'flex flex-col flex-1 min-h-0'}>
<FlowChat
onRunFlow={runChatTurn}
path={chatPath}
conversationKind="test"
subject="agent"
frame="none"
inputSchema={AGENT_CHAT_SCHEMA}
flowModules={chatModules}
/>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
{/snippet}
<!-- A tool is a whole step editor, so it gets a surface of its own rather than a level of the
@@ -141,7 +141,6 @@
on:change={() => dispatch('reload')}
agent={item}
{shareModal}
{moveDrawer}
{deploymentDrawer}
{depth}
bind:menuOpen
@@ -23,6 +23,8 @@
enableAi={$copilotInfo.enabled}
owns={(t) => t.host === undefined}
onClose={(deployed) => goto(deployed ? `${base}/agents/get/${path}` : `${base}/?kind=agent`)}
onDeployed={(saved) => goto(`${base}/agents/get/${saved}`)}
onDeployed={(saved) =>
// Replaced, not pushed: Back would otherwise reopen `new_draft` on an agent that now exists.
goto(`${base}/agents/get/${saved}`, { replaceState: isNew })}
/>
</div>
@@ -1,15 +1,6 @@
<script lang="ts">
import { page } from '$app/state'
import {
Bot,
FileUp,
FolderInput,
FormInput,
MessageSquare,
Pen,
Shield,
Trash
} from 'lucide-svelte'
import { Bot, FileUp, FormInput, MessageSquare, Pen, Shield, Trash } from 'lucide-svelte'
import { base } from '$lib/base'
import { goto } from '$lib/navigation'
import { copilotInfo } from '$lib/aiStore'
@@ -19,7 +10,6 @@
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import AgentEditorHost from '$lib/components/flows/content/AgentEditorHost.svelte'
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
@@ -43,9 +33,19 @@
let loaded = $derived(agent?.state != undefined)
let chatAvailable = $derived(keepsManagedMemory(agent?.state?.args?.memory))
let canEdit = $derived(loaded && (agent?.canWrite ?? false) && !$userStore?.operator)
// An agent runs as a flow preview, and the server refuses a preview to an operator, and to a
// non-admin under another user's namespace (`require_path_read_access_for_preview`).
let runBlockedReason = $derived(
$userStore?.operator
? 'Operators cannot run agents yet.'
: !$userStore?.is_admin &&
path.startsWith('u/') &&
!path.startsWith(`u/${$userStore?.username}/`)
? `Only its owner and workspace admins can run an agent under ${path.split('/').slice(0, 2).join('/')}.`
: undefined
)
let shareModal: ShareModal | undefined = $state(undefined)
let moveDrawer: MoveDrawer | undefined = $state(undefined)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state(undefined)
let deleteOpen = $state(false)
@@ -63,7 +63,6 @@
<ShareModal bind:this={shareModal} />
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<MoveDrawer bind:this={moveDrawer} on:update={(e) => goto(`${base}/agents/get/${e.detail}`)} />
<ConfirmationModal
open={deleteOpen}
title="Delete agent"
@@ -91,7 +90,7 @@
<div class="grow"></div>
<!-- Only an agent that keeps the conversation can chat: without managed memory every message
would be answered alone, so it is run from its inputs and nothing is offered to switch. -->
{#if loaded && chatAvailable && testPane?.mode}
{#if loaded && chatAvailable && !runBlockedReason && testPane?.mode}
<ToggleButtonGroup
bind:selected={
() => testPane?.mode,
@@ -131,12 +130,6 @@
disabled: !canEdit,
action: () => shareModal?.openDrawer?.(path, 'resource')
},
{
displayName: 'Move/Rename',
icon: FolderInput,
disabled: !canEdit,
action: () => moveDrawer?.openDrawer(path, agent?.state?.description, 'resource')
},
...(!agent?.state?.wsSpecific &&
isDeployable('resource', path, await getDeployUiSettings())
? [
@@ -178,6 +171,7 @@
{toolId}
onSelectTool={(id) => (toolId = id)}
view
{runBlockedReason}
/>
{/key}
</div>