fix: give the agent page's API and Slack sections tabs on small screens (#11901)

* fix: give the agent page's API and Slack panel a tab on small screens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the agent pane mounted across the small-screen breakpoint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: make the covered agent pane inert on the API tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: flatten the agent page's API and Slack sections into its small-screen tabs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: fall back to the API tab when the agent has no Slack section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-10-20 19:07:36 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e6c8ef18a1
commit 394d345e0c
2 changed files with 98 additions and 57 deletions
@@ -41,7 +41,8 @@
runsAs,
anyone,
canShare,
channels
channels,
section
}: {
agentPath: string
workspace: string
@@ -65,10 +66,12 @@
/** Whether the user can change its Slack settings: anyone who can edit it. */
canShare: boolean
channels: SlackAgentChannel[]
/** Shows only this section, without the panel's own tabs: the page offers them as its tabs. */
section?: 'api' | 'slack'
} = $props()
let pickedTab = $state('api')
let tab = $derived(chat ? pickedTab : 'api')
let tab = $derived(section ?? (chat ? pickedTab : 'api'))
let handle = $derived(`+${agentPath.split('/').pop()}`)
let userSettings: UserSettings | undefined = $state(undefined)
@@ -100,7 +103,11 @@
/>
<div class="flex flex-col h-full">
<Tabs bind:selected={() => tab, (t) => (pickedTab = t)} wrapperClass="flex-none w-full">
<Tabs
bind:selected={() => tab, (t) => (pickedTab = t)}
hideTabs={section !== undefined}
wrapperClass="flex-none w-full"
>
<Tab value="api" label="API" />
{#if chat}
<Tab value="slack" label="Slack" />
@@ -18,7 +18,7 @@
import AgentEditorHost from '$lib/components/flows/content/AgentEditorHost.svelte'
import AgentConfigModal from '$lib/components/flows/content/AgentConfigModal.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import { Alert } from '$lib/components/common'
import { Alert, Tab, Tabs } from '$lib/components/common'
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
import { agentMenuItems, deleteAgent } from '$lib/components/flows/agentActions'
import AgentHistory from '$lib/components/flows/AgentHistory.svelte'
@@ -49,6 +49,10 @@
let deleteOpen = $state(false)
let evalsModal: AgentEvalsModal | undefined = $state(undefined)
let clientWidth = $state(window.innerWidth)
let pickedMobileTab = $state('agent')
// The page is reused across agents, and an agent without managed memory has no Slack tab.
let mobileTab = $derived(pickedMobileTab === 'slack' && !chatAvailable ? 'api' : pickedMobileTab)
let wide = $derived(clientWidth >= 1024)
const slack = resource(
() => ({ workspace: ws, path }),
@@ -62,7 +66,7 @@
// while the chat shows, and the chat's composer owns the key then.
function onKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || event.key !== 'Enter' || !(event.ctrlKey || event.metaKey)) return
if (!runForm || testPane?.mode === 'chat') return
if (!runForm || testPane?.mode === 'chat' || (!wide && mobileTab !== 'agent')) return
if (isValid) {
event.preventDefault()
runForm.run()
@@ -207,67 +211,97 @@
{/if}
{/snippet}
</DetailPageHeader>
<div class="flex-1 min-h-0" bind:clientWidth>
<!-- Below `lg` the split would squeeze the chat, so the panel's sections become tabs of the
page, as on a script's or a flow's page. They cover the agent rather than replacing it:
unmounting the host destroys the chat and any turn in flight, so the agent pane keeps one
parent at every width. -->
{#if !wide && ws && config}
<Tabs
bind:selected={() => mobileTab, (t) => (pickedMobileTab = t)}
wrapperClass="flex-none no-scrollbar"
>
<Tab value="agent" label={testPane?.mode === 'chat' ? 'Chat' : 'Run form'} />
<Tab value="api" label="API" />
{#if chatAvailable}
<Tab value="slack" label="Slack" />
{/if}
</Tabs>
{/if}
<div class="relative flex-1 min-h-0" bind:clientWidth>
<Splitpanes>
<Pane size={65} minSize={50}>
{#key `${ws}:${path}:${reloaded}`}
<AgentEditorHost
bind:this={host}
{path}
workspace={ws}
enableAi={$copilotInfo.enabled}
view
onOpenConfig={() => configModal?.open()}
>
{#snippet viewForm({ schema, run, loading, actions })}
{#if config && !chatAvailable}
<Alert type="info" size="xs" title="Managed memory is off" class="mb-4">
Each run is answered on its own inputs, so this agent has no chat. That suits
reusing it as a step in a flow.{canEdit
? ' Turn on managed memory in its configuration to chat with it.'
: ''}
</Alert>
{/if}
<RunForm
runnable={{ schema, path }}
runAction={run}
schedulable={false}
detailed={false}
autofocus
{loading}
{actions}
bind:isValid
bind:this={runForm}
/>
{/snippet}
</AgentEditorHost>
{/key}
<Pane size={wide ? 65 : 100} minSize={50}>
<div class="h-full" inert={!wide && mobileTab !== 'agent'}>
{@render agentPane()}
</div>
</Pane>
<!-- ponytail: no panel below `lg`, where the split would squeeze the chat; give it a tab
there, as a script's page does, once people use it from small screens. -->
{#if ws && config && clientWidth >= 1024}
{#if wide && ws && config}
<Pane size={35} minSize={15}>
<AgentUsePanel
agentPath={path}
workspace={ws}
isAdmin={!!($userStore?.is_admin || $userStore?.is_super_admin)}
chat={chatAvailable}
slackLoading={slack.loading && !slack.current}
slackTeamName={slack.current?.slack_team_name}
botName={slack.current?.bot_name}
missingScopes={slack.current?.missing_scopes ?? []}
signingSecretSet={slack.current?.signing_secret_set ?? true}
runsAs={slack.current?.runs_as}
anyone={slack.current?.anyone ?? false}
canShare={slack.current?.can_share ?? false}
channels={slack.current?.channels ?? []}
/>
{@render usePane(ws)}
</Pane>
{/if}
</Splitpanes>
{#if !wide && ws && config}
<div class={['absolute inset-0 z-10 bg-surface', mobileTab === 'agent' && 'hidden']}>
{@render usePane(ws, mobileTab === 'slack' ? 'slack' : 'api')}
</div>
{/if}
</div>
</main>
{#snippet agentPane()}
{#key `${ws}:${path}:${reloaded}`}
<AgentEditorHost
bind:this={host}
{path}
workspace={ws}
enableAi={$copilotInfo.enabled}
view
onOpenConfig={() => configModal?.open()}
>
{#snippet viewForm({ schema, run, loading, actions })}
{#if config && !chatAvailable}
<Alert type="info" size="xs" title="Managed memory is off" class="mb-4">
Each run is answered on its own inputs, so this agent has no chat. That suits reusing it
as a step in a flow.{canEdit
? ' Turn on managed memory in its configuration to chat with it.'
: ''}
</Alert>
{/if}
<RunForm
runnable={{ schema, path }}
runAction={run}
schedulable={false}
detailed={false}
autofocus
{loading}
{actions}
bind:isValid
bind:this={runForm}
/>
{/snippet}
</AgentEditorHost>
{/key}
{/snippet}
{#snippet usePane(ws: string, section?: 'api' | 'slack')}
<AgentUsePanel
agentPath={path}
workspace={ws}
isAdmin={!!($userStore?.is_admin || $userStore?.is_super_admin)}
chat={chatAvailable}
slackLoading={slack.loading && !slack.current}
slackTeamName={slack.current?.slack_team_name}
botName={slack.current?.bot_name}
missingScopes={slack.current?.missing_scopes ?? []}
signingSecretSet={slack.current?.signing_secret_set ?? true}
runsAs={slack.current?.runs_as}
anyone={slack.current?.anyone ?? false}
canShare={slack.current?.can_share ?? false}
channels={slack.current?.channels ?? []}
{section}
/>
{/snippet}
{#if ws}
<AgentEvalsModal bind:this={evalsModal} agentPath={path} workspace={ws} />
{/if}