11<script setup lang="ts">
2- import type { DevframeClientCommand , DevframeCommandEntry } from ' @devframes/hub'
2+ import type { DevframeClientCommand } from ' @devframes/hub'
33import type { DocksContext } from ' @devframes/hub/client'
44import type { PaletteCrumb , PaletteFlatItem } from ' ../../state/palette'
55import Fuse from ' fuse.js'
66import { computed , nextTick , ref , useTemplateRef , watch } from ' vue'
7- import { flattenPaletteCommands , paletteScopeTrail } from ' ../../state/palette'
7+ import { flattenPaletteCommands , paletteActionKeepsOpen , paletteScopeTrail , paletteTrailScopeId , reconcilePaletteTrail , resolvePaletteSelection } from ' ../../state/palette'
88import BrandWordmark from ' ../icons/BrandWordmark.vue'
99import CommandPaletteItem from ' ./CommandPaletteItem.vue'
1010
@@ -70,7 +70,10 @@ function showScope(scopeId: string | null) {
7070 search .value = ' '
7171 selectedIndex .value = 0
7272 dynamicItems .value = undefined
73- breadcrumb .value = paletteScopeTrail (commandsCtx .value .paletteCommands , scopeId )
73+ const next = paletteScopeTrail (commandsCtx .value .paletteCommands , scopeId )
74+ breadcrumb .value = next
75+ if (scopeId != null && ! next .some (crumb => crumb .id === scopeId ))
76+ commandsCtx .value .paletteScopeId = null
7477}
7578
7679watch (show , (v ) => {
@@ -96,10 +99,25 @@ watch(show, (v) => {
9699// group picked from the root list, say. `show` stays `true` throughout, so the
97100// drill-down follows the scope itself rather than the open transition.
98101watch (() => commandsCtx .value .paletteScopeId , (scopeId ) => {
99- if (show .value && scopeId )
102+ if (show .value )
100103 showScope (scopeId )
101104})
102105
106+ // A command tree can change while the palette is open (dock registration,
107+ // `when` context, or a client command update). Rebuild each crumb by id so the
108+ // rendered rows and their actions always come from the live tree.
109+ watch (() => commandsCtx .value .paletteCommands , (commands ) => {
110+ if (! show .value || breadcrumb .value .length === 0 )
111+ return
112+ const scopeId = commandsCtx .value .paletteScopeId
113+ const scopeWasActive = scopeId != null && breadcrumb .value .some (crumb => crumb .id === scopeId )
114+ const next = reconcilePaletteTrail (commands , breadcrumb .value , scopeId )
115+ breadcrumb .value = next
116+ selectedIndex .value = Math .min (selectedIndex .value , Math .max (filtered .value .length - 1 , 0 ))
117+ if (scopeWasActive && scopeId != null && ! next .some (crumb => crumb .id === scopeId ))
118+ commandsCtx .value .paletteScopeId = null
119+ })
120+
103121function moveSelected(delta : number ) {
104122 const len = filtered .value .length
105123 if (len === 0 )
@@ -120,17 +138,16 @@ function scrollToItem() {
120138const loadingId = ref <string | null >(null )
121139
122140async function enterItem(flatItem : PaletteFlatItem ) {
123- const entry = flatItem .entry
141+ // The row may have been rendered just before the command tree changed. Look
142+ // it up again so a removed entry no-ops and a replacement runs its new action.
143+ const entry = activeItems .value .find (item => item .entry .id === flatItem .entry .id )?.entry
144+ if (! entry )
145+ return
124146
125- // If has static children, drill down
126- if (entry .children && entry .children .length > 0 ) {
127- breadcrumb .value .push ({
128- title: entry .title ,
129- items: entry .children as DevframeCommandEntry [],
130- })
131- search .value = ' '
132- selectedIndex .value = 0
133- dynamicItems .value = undefined
147+ // Ordinary command parents drill down. Dock groups are actionable parents:
148+ // their action opens a preferred member or scopes the palette for a choice.
149+ if (resolvePaletteSelection (entry , props .context .docks .entries ) === ' drill' ) {
150+ commandsCtx .value .paletteScopeId = entry .id
134151 return
135152 }
136153
@@ -149,6 +166,8 @@ async function enterItem(flatItem: PaletteFlatItem) {
149166 catch (err ) {
150167 console .error (` [@devframes/hub-ui] Command "${entry .id }" failed: ` , err )
151168 }
169+ if (paletteActionKeepsOpen (entry , props .context .docks .entries , commandsCtx .value .paletteOpen , commandsCtx .value .paletteScopeId ))
170+ return
152171 close ()
153172 return
154173 }
@@ -193,7 +212,7 @@ function goBack() {
193212 }
194213 if (breadcrumb .value .length > 0 ) {
195214 breadcrumb .value .pop ()
196- dropScopeAtRoot ( )
215+ commandsCtx . value . paletteScopeId = paletteTrailScopeId ( breadcrumb . value )
197216 search .value = ' '
198217 selectedIndex .value = 0
199218 return
@@ -204,21 +223,11 @@ function goBack() {
204223/** Jump to the level the crumb at `index` sits above. */
205224function goToCrumb(index : number ) {
206225 breadcrumb .value .splice (index )
207- dropScopeAtRoot ( )
226+ commandsCtx . value . paletteScopeId = paletteTrailScopeId ( breadcrumb . value )
208227 search .value = ' '
209228 selectedIndex .value = 0
210229}
211230
212- /**
213- * Stepping back out to the root list leaves the palette unscoped, so the
214- * shortcut that scoped it drills back in instead of reading as "press again to
215- * close".
216- */
217- function dropScopeAtRoot() {
218- if (breadcrumb .value .length === 0 )
219- commandsCtx .value .paletteScopeId = null
220- }
221-
222231function onKeyDown(e : KeyboardEvent ) {
223232 if (e .key === ' Backspace' && ! search .value && (breadcrumb .value .length > 0 || dynamicItems .value )) {
224233 e .preventDefault ()
0 commit comments