mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 08:02:44 +00:00
fix: open the chat + menu without waiting on the skills and mcp fetches (#11645)
* fix: resolve dropdown items before showing the menu on first open * fix: open the chat + menu without waiting on the skills and mcp fetches * fix: keep mcp submenu rows in place when toggling a server
This commit is contained in:
@@ -1107,16 +1107,13 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. -->
|
||||
</Tooltip>
|
||||
{:else if canAttachFiles}
|
||||
<DropdownV2
|
||||
items={async () => {
|
||||
// Both submenus fetch on the menu's first open, so they start
|
||||
// together: awaited inline they queue, and the whole menu —
|
||||
// attachments included — waits out two round trips.
|
||||
items={() => {
|
||||
const closeMenu = () => (plusMenuOpen = false)
|
||||
const inGlobal = chatHost.mode === AIMode.GLOBAL
|
||||
const [skillItems, mcpItems] = await Promise.all([
|
||||
inGlobal ? skillsMenu.items(closeMenu) : undefined,
|
||||
inGlobal ? mcpMenu.items(closeMenu) : undefined
|
||||
])
|
||||
if (inGlobal) {
|
||||
skillsMenu.refresh()
|
||||
mcpMenu.refresh()
|
||||
}
|
||||
return [
|
||||
{
|
||||
displayName: 'Attach file or image',
|
||||
@@ -1156,23 +1153,24 @@ the panel, or the Escape-to-stop focus check would wrongly reject them. -->
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(skillItems
|
||||
...(inGlobal
|
||||
? [
|
||||
{
|
||||
displayName: 'Skills',
|
||||
icon: BookOpen,
|
||||
separatorTop: true,
|
||||
submenuItems: skillItems
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(mcpItems
|
||||
? [
|
||||
// Getters so the submenus fill in when their fetch lands
|
||||
// rather than the whole menu waiting on it.
|
||||
get submenuItems() {
|
||||
return skillsMenu.items(closeMenu)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'MCP connections',
|
||||
icon: Plug,
|
||||
separatorTop: !skillItems,
|
||||
submenuItems: mcpItems
|
||||
get submenuItems() {
|
||||
return mcpMenu.items(closeMenu)
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookOpen, List, Plus } from 'lucide-svelte'
|
||||
import { BookOpen, List, Loader2, Plus } from 'lucide-svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -14,6 +14,14 @@ type Row = SkillResource & { enabled: boolean }
|
||||
// scannable, so the rest are reached through the settings modal rather than dropped.
|
||||
const MAX_MENU_SKILLS = 8
|
||||
|
||||
// Not `disabled`: melt stamps that on a row at mount and never clears it, and the
|
||||
// unkeyed submenu reuses this row for the first skill once the load lands.
|
||||
const loadingItem: Item = {
|
||||
displayName: 'Loading…',
|
||||
icon: Loader2,
|
||||
iconProps: { class: 'animate-spin shrink-0' }
|
||||
}
|
||||
|
||||
/**
|
||||
* The chat "+" menu's Skills submenu: one row per skill, checked when it is on,
|
||||
* then the way to manage them. Everything a skill is beyond turning it on and off
|
||||
@@ -25,7 +33,7 @@ export class SkillsMenu {
|
||||
#seq = 0
|
||||
/** Rows for the workspace named by `#rowsWorkspace`, and meaningless for any other. */
|
||||
#rows = $state<Row[]>([])
|
||||
#rowsWorkspace: string | undefined = undefined
|
||||
#rowsWorkspace = $state<string | undefined>(undefined)
|
||||
|
||||
constructor(manager: AIChatManager, onManage: () => void) {
|
||||
this.#manager = manager
|
||||
@@ -89,19 +97,21 @@ export class SkillsMenu {
|
||||
await this.#manager.refreshGlobalSkills(ws)
|
||||
}
|
||||
|
||||
/** Loaded on open so the checks are current. */
|
||||
async items(closeMenu?: () => void): Promise<Item[]> {
|
||||
/** Called on open so the checks are current; never awaited, see `items`. */
|
||||
refresh() {
|
||||
const ws = this.#ws
|
||||
if (ws) void this.#load(ws)
|
||||
}
|
||||
|
||||
/**
|
||||
* Built from the live rows, so call it from a getter: the submenu then fills in
|
||||
* when a load lands instead of the whole "+" menu waiting on the fetch. Rows for
|
||||
* another workspace are not shown — same path, different skill.
|
||||
*/
|
||||
items(closeMenu?: () => void): Item[] {
|
||||
const ws = this.#ws
|
||||
if (!ws) return []
|
||||
// The menu opens on what is already known and refreshes behind it: awaited
|
||||
// inline it would stall the whole "+" menu, attachments included. Rows for
|
||||
// another workspace are not "already known" — same path, different skill.
|
||||
if (this.#rowsWorkspace !== ws) {
|
||||
this.#rows = []
|
||||
await this.#load(ws)
|
||||
} else {
|
||||
void this.#load(ws)
|
||||
}
|
||||
if (this.#rowsWorkspace !== ws) return [loadingItem]
|
||||
const ambiguous = ambiguousSkillNames(this.#rows)
|
||||
// By path. Ordering the ones that are on first would put every row in the same
|
||||
// bucket now that skills start that way, and drop the one row it did move — a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { List, Plus } from 'lucide-svelte'
|
||||
import { List, Loader2, Plus } from 'lucide-svelte'
|
||||
import type { Component } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
@@ -27,6 +27,14 @@ const MAX_MENU_SERVERS = 8
|
||||
// menu nobody is reading that far down.
|
||||
const MAX_ICON_LOOKUPS = 20
|
||||
|
||||
// Not `disabled`: melt stamps that on a row at mount and never clears it, and the
|
||||
// unkeyed submenu reuses this row for the first server once the load lands.
|
||||
const loadingItem: Item = {
|
||||
displayName: 'Loading…',
|
||||
icon: Loader2,
|
||||
iconProps: { class: 'animate-spin shrink-0' }
|
||||
}
|
||||
|
||||
/**
|
||||
* The chat "+" menu's MCP submenu: one row per connected server, checked when it
|
||||
* is on, then the way to manage them. Connecting and deleting live in the
|
||||
@@ -38,7 +46,7 @@ export class McpMenu {
|
||||
#seq = 0
|
||||
/** Rows for the workspace named by `#rowsWorkspace`, and meaningless for any other. */
|
||||
#rows = $state<Row[]>([])
|
||||
#rowsWorkspace: string | undefined = undefined
|
||||
#rowsWorkspace = $state<string | undefined>(undefined)
|
||||
|
||||
constructor(manager: AIChatManager, onManage: () => void) {
|
||||
this.#manager = manager
|
||||
@@ -73,6 +81,10 @@ export class McpMenu {
|
||||
editedAt: r.edited_at,
|
||||
enabled: isMcpEnabled(ws, r.path)
|
||||
}))
|
||||
// Enabled first: those are the ones a quick visit is most likely about.
|
||||
// Sorted here, once per load, not in `items()`: the open submenu re-derives
|
||||
// from `items()`, so a toggle would move rows under the cursor.
|
||||
.sort((a, b) => Number(b.enabled) - Number(a.enabled) || a.path.localeCompare(b.path))
|
||||
this.#rowsWorkspace = ws
|
||||
void this.#loadIcons(ws, seq)
|
||||
} catch {
|
||||
@@ -143,23 +155,22 @@ export class McpMenu {
|
||||
await this.#manager.refreshMcpServers(ws)
|
||||
}
|
||||
|
||||
/** Loaded on open so the checks are current. */
|
||||
async items(closeMenu?: () => void): Promise<Item[]> {
|
||||
/** Called on open so the checks are current; never awaited, see `items`. */
|
||||
refresh() {
|
||||
const ws = this.#ws
|
||||
if (ws) void this.#load(ws)
|
||||
}
|
||||
|
||||
/**
|
||||
* Built from the live rows, so call it from a getter: the submenu then fills in
|
||||
* when a load lands instead of the whole "+" menu waiting on the fetch. Rows for
|
||||
* another workspace are not shown — same path, different server.
|
||||
*/
|
||||
items(closeMenu?: () => void): Item[] {
|
||||
const ws = this.#ws
|
||||
if (!ws) return []
|
||||
// The menu opens on what is already known and refreshes behind it: awaited
|
||||
// inline it would stall the whole "+" menu, attachments included. Rows for
|
||||
// another workspace are not "already known" — same path, different server.
|
||||
if (this.#rowsWorkspace !== ws) {
|
||||
this.#rows = []
|
||||
await this.#load(ws)
|
||||
} else {
|
||||
void this.#load(ws)
|
||||
}
|
||||
// Enabled first: those are the ones a quick visit is most likely about.
|
||||
const ordered = [...this.#rows].sort(
|
||||
(a, b) => Number(b.enabled) - Number(a.enabled) || a.path.localeCompare(b.path)
|
||||
)
|
||||
if (this.#rowsWorkspace !== ws) return [loadingItem]
|
||||
const ordered = this.#rows
|
||||
const shown = ordered.slice(0, MAX_MENU_SERVERS)
|
||||
const manage = () => {
|
||||
closeMenu?.()
|
||||
|
||||
Reference in New Issue
Block a user