mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
revert: keep operators out of AI sessions
That change is its own PR; this one is about the navigation. `prefersSessionHandoff` takes the role again and every entry point passes it, the sessions page refuses an operator arriving by URL, and the layout leaves them on the docked chat. Two clauses are new rather than restored, because the nav rework is what gives an operator a sidebar at all: the Workspace/AI Sessions switch and session mode are both hidden from them, since the page behind them declines. `askAiShown` gains the operator case for the same reason — they keep the docked pane whatever the beta gate says, so the sidebar has to offer a way into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae932e6b79
commit
0c40295bec
@@ -5,6 +5,7 @@
|
||||
import { AIBtnClasses } from './chat/AIButtonStyle'
|
||||
import { prefersSessionHandoff } from './chat/global/gate'
|
||||
import { startSessionWithPrompt } from '$lib/components/sessions/sessionSwitch.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
interface Props {
|
||||
label?: string
|
||||
initialInput?: string
|
||||
@@ -15,7 +16,7 @@
|
||||
|
||||
// The label stays short ("Ask AI") for the search bar's inline row; the hover
|
||||
// text is where "new AI session" fits.
|
||||
const handsOffToSession = $derived(prefersSessionHandoff())
|
||||
const handsOffToSession = $derived(prefersSessionHandoff($userStore?.operator))
|
||||
|
||||
export function onClick() {
|
||||
// No item to preview here — this carries a question, not a target — so the
|
||||
|
||||
@@ -29,9 +29,10 @@ export function isGlobalAiEnabled(): boolean {
|
||||
* Whether an AI entry point hands off to a session instead of driving the docked
|
||||
* chat. Deliberately the same condition as the root layout's `disableAi`, so a
|
||||
* caller falling back on `false` always has a mounted pane to fall back to.
|
||||
* Operators keep that pane (`/sessions` refuses them) until the operator chat ships.
|
||||
*/
|
||||
export function prefersSessionHandoff(): boolean {
|
||||
return isGlobalAiEnabled()
|
||||
export function prefersSessionHandoff(isOperator: boolean | undefined): boolean {
|
||||
return isGlobalAiEnabled() && !isOperator
|
||||
}
|
||||
|
||||
/** Clear the opt-out, then hard-reload so every gated site re-reads it. */
|
||||
|
||||
@@ -40,9 +40,14 @@
|
||||
import FlowPanelPlacementPicker from './common/FlowPanelPlacementPicker.svelte'
|
||||
import { prefersSessionHandoff } from '../copilot/chat/global/gate'
|
||||
import { openSourceInSession } from '$lib/components/sessions/sessionSwitch.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import {
|
||||
useOperatingUser,
|
||||
useOperatingWorkspace
|
||||
} from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
const { flowStore, selectionManager, pathStore, opWorkspace } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
// Flow paths repeat across workspaces, and a session keeps every tab it has visited alive, so two
|
||||
@@ -403,7 +408,11 @@
|
||||
// contain. Hand it to a session opened on that step rather than the
|
||||
// docked chat, which sessions leave unmounted. Sent on arrival: the
|
||||
// user already said what they wanted in the description field.
|
||||
if (!sessionScopedManager && sessionOpen && prefersSessionHandoff()) {
|
||||
if (
|
||||
!sessionScopedManager &&
|
||||
sessionOpen &&
|
||||
prefersSessionHandoff(actingUser?.operator)
|
||||
) {
|
||||
void openSourceInSession(sessionOpen, {
|
||||
previewParams: { selected: detail.moduleId },
|
||||
seedPrompt: stepInstructionsPrompt(detail.moduleId, detail.instructions),
|
||||
|
||||
@@ -147,10 +147,13 @@
|
||||
// unlocked, so the far commoner unlocked workspace never pops the composer in mid-load.
|
||||
let runOnlyWorkspace = $derived(isRuleActive('DisableDirectDeployment'))
|
||||
|
||||
// The composer hands off to /sessions, so users opted out of the sessions beta don't get it
|
||||
// (the prompt would be silently dropped) while the AI-independent CLI/MCP row below stays.
|
||||
// The composer hands off to /sessions, which refuses operators and users opted out of the
|
||||
// sessions beta — so hide it from them (the prompt would be silently dropped) while the
|
||||
// AI-independent CLI/MCP row below stays.
|
||||
let showComposer = $derived(
|
||||
prefersSessionHandoff() && !runOnlyWorkspace && !$copilotInfo.workspaceDisabled
|
||||
prefersSessionHandoff($userStore?.operator) &&
|
||||
!runOnlyWorkspace &&
|
||||
!$copilotInfo.workspaceDisabled
|
||||
)
|
||||
|
||||
// The hero's margins and centered column are for the full block. The lone button row left
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
const aiConfigLoaded = $derived(!!opWs && $copilotWorkspace === opWs)
|
||||
// Say where the button leads: the route hands this prompt to a fresh AI
|
||||
// session for everyone who has one, and drives the docked chat for the rest.
|
||||
const handsOffToSession = $derived(prefersSessionHandoff())
|
||||
const handsOffToSession = $derived(prefersSessionHandoff($userStore?.operator))
|
||||
const isAiEnabled = $derived(aiConfigLoaded && $copilotInfo.enabled)
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { openSourceInSession } from './sessionSwitch.svelte'
|
||||
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingUser = useOperatingUser()
|
||||
const actingUser = $derived(operatingUser.current)
|
||||
|
||||
let {
|
||||
source,
|
||||
@@ -76,8 +80,13 @@
|
||||
// SessionEditorTarget / the session wrapper); iframe preview tabs are not
|
||||
// the top window.
|
||||
const inSessionPanel = !!getContext('aiChatManager') || (BROWSER && window.self !== window.top)
|
||||
// prefersSessionHandoff carries the operator clause: the sessions page refuses
|
||||
// them, so an entry point on a page they can reach (Runs, the trigger lists)
|
||||
// would only route them into that refusal.
|
||||
const show = $derived(
|
||||
!inSessionPanel && !!(source?.target || source?.page) && prefersSessionHandoff()
|
||||
!inSessionPanel &&
|
||||
!!(source?.target || source?.page) &&
|
||||
prefersSessionHandoff(actingUser?.operator)
|
||||
)
|
||||
|
||||
// Not $state: only read inside open() as a re-entrancy latch, never rendered.
|
||||
|
||||
@@ -65,10 +65,7 @@
|
||||
getFavoriteHref,
|
||||
getFavoriteLabel
|
||||
} from '$lib/components/sidebar/FavoriteMenu.svelte'
|
||||
import {
|
||||
parseSuperadminSettingsHash,
|
||||
USER_SETTINGS_HASH
|
||||
} from '$lib/components/sidebar/settings'
|
||||
import { parseSuperadminSettingsHash, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import {
|
||||
PanelLeft,
|
||||
@@ -302,7 +299,11 @@
|
||||
// Session mode is route-derived: the rail shows the sessions sidebar on the
|
||||
// /sessions page and the workspace navigation everywhere else. The switch
|
||||
// (SessionModeSwitch) just navigates in and out of that route.
|
||||
let sessionMode = $derived(page.url.pathname.startsWith(base + '/sessions'))
|
||||
// The route decides, except for an operator: /sessions refuses them, so handing their
|
||||
// sidebar over to a session list would offer what the page behind it is declining.
|
||||
let sessionMode = $derived(
|
||||
page.url.pathname.startsWith(base + '/sessions') && !$userStore?.operator
|
||||
)
|
||||
// Session mode points the bottom settings entry at the open session's own
|
||||
// workspace. An unsent draft hasn't committed one yet, so it falls back to
|
||||
// the family root.
|
||||
@@ -323,8 +324,14 @@
|
||||
const globalAiEnabled = isGlobalAiEnabled()
|
||||
// A workspace that hid the assistant (`ai_config.copilot_disabled`) loses both entry
|
||||
// points: the Workspace ⇄ Sessions switch and the legacy Ask-AI button.
|
||||
const sessionsSwitchShown = $derived(globalAiEnabled && !$copilotInfo.workspaceDisabled)
|
||||
const askAiShown = $derived(!globalAiEnabled && !$copilotInfo.workspaceDisabled)
|
||||
const sessionsSwitchShown = $derived(
|
||||
globalAiEnabled && !$copilotInfo.workspaceDisabled && !$userStore?.operator
|
||||
)
|
||||
// The legacy pane's entry point. An operator keeps that pane whatever the beta gate says
|
||||
// (`disableAi` above), so the sidebar must still offer them a way into it.
|
||||
const askAiShown = $derived(
|
||||
(!globalAiEnabled || $userStore?.operator) && !$copilotInfo.workspaceDisabled
|
||||
)
|
||||
|
||||
if (page.status == 404) {
|
||||
goto('/user/login')
|
||||
@@ -903,7 +910,7 @@
|
||||
$effect(() => {
|
||||
const ws = $workspaceStore
|
||||
const ready = sessionState.hydrated && $usersWorkspaceStore !== undefined
|
||||
if (globalAiEnabled && ready && ws) {
|
||||
if (globalAiEnabled && ready && ws && !$userStore?.operator) {
|
||||
untrack(() => restoreSessionBackups(ws))
|
||||
}
|
||||
})
|
||||
@@ -1679,9 +1686,9 @@
|
||||
<AiChatLayout
|
||||
{children}
|
||||
noPadding={devOnly || menuHidden}
|
||||
disableAi={globalAiEnabled ? true : sessionMode}
|
||||
disableAi={globalAiEnabled && !$userStore?.operator ? true : sessionMode}
|
||||
loadAiConfig={!sessionMode}
|
||||
showSessionsBetaBanner
|
||||
showSessionsBetaBanner={!$userStore?.operator}
|
||||
sidebarWidth={railWidth}
|
||||
transitionClass={sidebarTransitionClass}
|
||||
isMobile={useDrawer}
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
setTimeout(async () => {
|
||||
// Falls through when the hand-off has no path to open, so the click
|
||||
// still reaches the legacy path (or its toast) instead of vanishing.
|
||||
if (prefersSessionHandoff()) {
|
||||
if (prefersSessionHandoff($userStore?.operator)) {
|
||||
if (await rawAppEditor?.openInSession(prompt)) return
|
||||
}
|
||||
aiChatManager.changeMode(AIMode.APP)
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
import { withWorkspaceParam } from '$lib/components/sessions/sessionMode.svelte'
|
||||
import { enterSessionMode } from '$lib/components/sessions/sessionSwitch.svelte'
|
||||
import type { SessionPreviewTabs } from '$lib/components/sessions/sessionPreviewTabs.svelte'
|
||||
import { userWorkspaces, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import { userStore, userWorkspaces, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
getOrCreateRuntime,
|
||||
getRuntime,
|
||||
@@ -197,7 +197,7 @@
|
||||
// load. `recovering` also guards re-entry: recovery mutates the session list
|
||||
// this effect tracks, while the URL that would stop it only updates on `goto`.
|
||||
$effect(() => {
|
||||
if (embedded || !globalEnabled) return
|
||||
if (embedded || !globalEnabled || $userStore?.operator) return
|
||||
if (!sessionState.hydrated || recovering) return
|
||||
// A deliberate delete removes the open session ahead of its own navigation.
|
||||
// Claiming that gap would take over the URL and tell the user the session
|
||||
@@ -910,6 +910,24 @@
|
||||
}}>Open sessions</Button
|
||||
>
|
||||
</div>
|
||||
{:else if $userStore?.operator}
|
||||
<!-- Operators are exempt from the sessions beta (the layout keeps their
|
||||
legacy docked chat); a direct URL must not bypass that. -->
|
||||
<div class="p-8 flex flex-col items-start gap-3 text-secondary text-sm">
|
||||
<p class="text-primary font-medium">AI Sessions are not available for operators</p>
|
||||
<p>Use the Ask AI chat instead.</p>
|
||||
<Button
|
||||
unifiedSize="xs"
|
||||
onclick={() => {
|
||||
try {
|
||||
localStorage.setItem('ai-chat-open', 'true')
|
||||
} catch {}
|
||||
window.location.href = `${base}/`
|
||||
}}
|
||||
>
|
||||
Open Ask AI chat
|
||||
</Button>
|
||||
</div>
|
||||
{:else if !globalEnabled}
|
||||
<!-- Direct navigation (bookmark, shared link) while the user has opted out
|
||||
of the beta: offer the way back in instead of a dead end. -->
|
||||
|
||||
Reference in New Issue
Block a user