opencode-navigator 0.18.2 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [0.19.1] - 2026-10-02
6
+
7
+ - Remember the OpenCode 1.x auto-approve mode as a user-wide default for new sessions, including changes made through the host's permission toggle.
8
+
9
+ ## [0.19.0] - 2026-10-02
10
+
11
+ - Connect or disconnect all eligible MCP servers in a group from its heading, including filtered-out items, with keyboard access and failed-only retry.
12
+ - Organize Skills into user-wide groups by source location, with Favorites first and group names searchable across the full catalog.
13
+ - Reload saved Navigator settings from disk in another running OpenCode session without restarting it.
14
+
5
15
  ## [0.18.2] - 2026-10-02
6
16
 
7
17
  - Open the host Settings dialog from the OpenCode 2.x Permissions Quick Action, where the user can change between `prompt` and `auto accept`; keep the direct auto-approve toggle on OpenCode 1.x.
@@ -45,6 +55,8 @@
45
55
 
46
56
  Earlier release notes are available on [GitHub Releases](https://github.com/St1ggy/opencode-navigator/releases).
47
57
 
58
+ [0.19.1]: https://github.com/St1ggy/opencode-navigator/compare/v0.19.0...v0.19.1
59
+ [0.19.0]: https://github.com/St1ggy/opencode-navigator/compare/v0.18.2...v0.19.0
48
60
  [0.18.2]: https://github.com/St1ggy/opencode-navigator/compare/v0.18.1...v0.18.2
49
61
  [0.18.1]: https://github.com/St1ggy/opencode-navigator/compare/v0.18.0...v0.18.1
50
62
  [0.18.0]: https://github.com/St1ggy/opencode-navigator/compare/v0.17.3...v0.18.0
package/README.md CHANGED
@@ -14,7 +14,7 @@ See the [changelog](CHANGELOG.md) for release-by-release changes.
14
14
  - First-run setup guide with interactive section settings
15
15
  - Collapsible Todo section with status filters, grouped tasks, progress and priority indicators; OpenCode 2 adds session-owned tasks and optional model-context instructions
16
16
  - Active subagent list with live statuses and click-to-open navigation, including dev-team workers running in separate local processes
17
- - Compact, searchable workspace skill list with user-wide favorites, recent skills, source details, and click-to-confirm slash commands
17
+ - Compact, searchable workspace skill list with user-wide favorites and groups, recent skills, source details, and click-to-confirm slash commands
18
18
  - Configurable host quick actions, including OpenCode 1.x's auto-approve toggle and OpenCode 2.x's Permissions settings, with user-wide bookmarks and availability reasons
19
19
  - Live LSP connection status with error-first sorting and badges that reveal server names in place
20
20
  - Searchable MCP section with live radio-style connection controls
@@ -85,11 +85,24 @@ muted section dividers share the same narrow sidebar:
85
85
  MCP groups are managed from **Settings → Sections → MCP → Groups**. Favorites
86
86
  remain in a leading bucket; custom groups and their servers sort alphabetically.
87
87
  Assignments can select an existing group or open an input to create a new one.
88
+ Click a group heading (including Favorites or Ungrouped), or focus it and press
89
+ `Enter`, to connect its remaining eligible servers. Once none need connecting,
90
+ the same control disconnects its connected servers. This affects the full group
91
+ even when a filter or item limit hides some rows; failed operations retain the
92
+ usual retry control.
88
93
 
89
94
  ![MCP group manager](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/31-mcp-groups.png)
90
95
 
91
96
  ![MCP error details](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/32-mcp-error.png)
92
97
 
98
+ Manage Skill groups from **Settings → Sections → Skills → Groups**. Favorites
99
+ stay first, named groups sort alphabetically, and Ungrouped stays last. Groups
100
+ belong to each Skill's exact source path, so assignments survive when a Skill
101
+ is absent from the current workspace. Filters and Search Everything also match
102
+ group names; choosing a group heading never runs its Skills.
103
+
104
+ ![Skill group manager](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/38-skill-groups.png)
105
+
93
106
  Search Everything preserves one query across Skills, Subagents, MCP, and Actions:
94
107
 
95
108
  ![Search Everything skills](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/09-search-skills.png)
@@ -124,7 +137,7 @@ the uninstalled-font result remains visible rather than being documented only in
124
137
 
125
138
  ![Nerd Font without Navigator corner font](https://raw.githubusercontent.com/St1ggy/opencode-navigator/main/screenshots/35-no-corner-font-layout-preview.png)
126
139
 
127
- The [complete 37-state screenshot gallery](screenshots/README.md) shows every
140
+ The [complete 38-state screenshot gallery](screenshots/README.md) shows every
128
141
  Todo and Subagent filter, sidebar section, Search and Settings tab, preset menu
129
142
  and preview, confirmation, keyboard-help, setup, and icon-fallback state.
130
143
 
@@ -325,6 +338,15 @@ Search Everything. Usage never changes the order. Visibility remains independent
325
338
  so any action, including a bookmarked one, can be hidden from the sidebar while it
326
339
  remains searchable. Unavailable actions stay visible with a route- or host-specific reason.
327
340
 
341
+ In OpenCode 1.x, Navigator saves changes to the auto-approve mode as a user-wide
342
+ default, including changes made through OpenCode's own command palette. Set the
343
+ default under **Navigator Settings → Behavior → Default auto-approve permissions**;
344
+ new sessions restore it. Until you set a Navigator default, an explicit OpenCode
345
+ startup mode is respected. OpenCode 2.x owns this setting: use the host Settings
346
+ dialog for the current session and `session.permissions` in the user-wide
347
+ `cli.json` (`"prompt"` or `"autoaccept"`) for its startup default. Restart
348
+ OpenCode after changing `cli.json`.
349
+
328
350
  ## Configuration sources
329
351
 
330
352
  Navigator validates and deep-merges three simultaneous sources in this order:
@@ -450,8 +472,13 @@ the terminal clipboard. **Import portable settings…** accepts pasted JSON and
450
472
  opens a preview before writing. Unknown future fields and section IDs are listed
451
473
  as skipped; malformed versions or invalid known values are rejected. Apply
452
474
  replaces only the supplied layout and MCP blocks in the still-selected scope in
453
- one atomic preferences update. Paths, shortcuts, favorites, history, trust choices,
475
+ one atomic preferences update. Paths, Skill group assignments, shortcuts, favorites, history, trust choices,
454
476
  and other private preferences are never imported or exported.
477
+ **Reload settings from file** reads Navigator's saved private preferences on
478
+ demand. It picks up layout defaults, behavior, favorites, groups, and presets
479
+ saved in another OpenCode session without restarting; temporary layout edits in
480
+ the receiving session are cleared so they cannot hide the reloaded settings.
481
+ Save a session's layout as the default before reloading it elsewhere.
455
482
 
456
483
  ## Keyboard controls
457
484
 
@@ -0,0 +1,24 @@
1
+ import {PresetMenu} from "./tui.js"
2
+ import {createComponent as _$createComponent} from "@opentui/solid"
3
+ import {useDialogs} from "./tui.js"
4
+ function GroupAssignmentDialog(props){const dialogs=useDialogs(props.api)
5
+ const options=()=>[...props.groups().map((group)=>({title:group,value:`:${group}`,description:group===props.assigned()?"Current":"Existing group"
6
+ })),{title:"Create new group",value:"+",description:"Type a name"
7
+ },{title:"Ungrouped",value:":",description:props.assigned()?"Remove assignment":"No group"
8
+ }]
9
+ return _$createComponent(PresetMenu,{get api(){return props.api
10
+ },get title(){return props.title
11
+ },get options(){return options()
12
+ },onSelect:(option)=>{if(option.value!=="+"){props.onAssign(option.value.slice(1)||undefined)
13
+ dialogs.back()
14
+ return
15
+ }
16
+ dialogs.prompt({title:props.title,value:"",onConfirm(value){if(!value.trim())
17
+ throw new Error("Group name is required")
18
+ props.onAssign(value)
19
+ dialogs.back()
20
+ dialogs.back()
21
+ }})
22
+ }})
23
+ }
24
+ export{PresetMenu,GroupAssignmentDialog}
@@ -0,0 +1,25 @@
1
+ import {PresetMenu,GroupAssignmentDialog
2
+ } from "./chunk-r37n7w97.js";
3
+ import {createComponent as _$createComponent2} from "@opentui/solid"
4
+ import {useDialogs} from "./tui.js"
5
+ import {createComponent as _$createComponent} from "@opentui/solid"
6
+ function McpGroupAssignmentDialog(props){return _$createComponent(GroupAssignmentDialog,{get api(){return props.api
7
+ },get title(){return `Group for ${props.server}`
8
+ },assigned:()=>props.preferences.mcpServerGroups?.()[props.server],groups:()=>[...new Set(Object.values(props.preferences.mcpServerGroups?.()??{}))].sort((a,b)=>a.localeCompare(b)),onAssign:(group)=>props.preferences.setMcpServerGroup?.(props.server,group)
9
+ })
10
+ }
11
+ function McpGroupsDialog(props){const dialogs=useDialogs(props.api)
12
+ const options=()=>{const live=new Set(props.mcp.list(props.mcp.target()).map((item)=>item.name))
13
+ return [...new Set([...live,...Object.keys(props.preferences.mcpServerGroups?.()??{})])].sort((a,b)=>a.localeCompare(b)).map((name)=>({title:name,value:name,description:[props.preferences.mcpServerGroups?.()[name]??"Ungrouped",!live.has(name)&&"not in workspace"].filter(Boolean).join(" \xB7 ")
14
+ }))
15
+ }
16
+ return _$createComponent2(PresetMenu,{get api(){return props.api
17
+ },title:"MCP groups \xB7 user-wide",get options(){return options()
18
+ },onSelect:(option)=>dialogs.open(()=>_$createComponent2(McpGroupAssignmentDialog,{get api(){return props.api
19
+ },get preferences(){return props.preferences
20
+ },get server(){return option.value
21
+ }}))
22
+ })
23
+ }
24
+ export{McpGroupsDialog
25
+ }
@@ -0,0 +1,162 @@
1
+ import {memo as _$memo} from "@opentui/solid"
2
+ import {createComponent as _$createComponent} from "@opentui/solid"
3
+ import {use as _$use} from "@opentui/solid"
4
+ import {effect as _$effect} from "@opentui/solid"
5
+ import {createTextNode as _$createTextNode} from "@opentui/solid"
6
+ import {insertNode as _$insertNode} from "@opentui/solid"
7
+ import {insert as _$insert} from "@opentui/solid"
8
+ import {setProp as _$setProp} from "@opentui/solid"
9
+ import {createElement as _$createElement} from "@opentui/solid"
10
+ import {TextAttributes} from "@opentui/core"
11
+ import {useTerminalDimensions} from "@opentui/solid"
12
+ import {For,createEffect,createMemo,onCleanup as onCleanup2} from "solid-js"
13
+ import {createSignal,onCleanup} from "solid-js"
14
+ var QUICK_ACTION_IDS=["session.rename","session.timeline","session.copy","session.export","session.compact","session.list","session.new","session.fork","messages.copy","session.first","session.last","session.messages_last_user","session.message.next","session.message.previous","permission.mode"];
15
+ var PLUGIN_ID="opencode-navigator"
16
+ var TOGGLE_COMMAND=`${PLUGIN_ID}.toggle`
17
+ var FOCUS_COMMAND=`${PLUGIN_ID}.focus`
18
+ var SEARCH_COMMAND=`${PLUGIN_ID}.search`;
19
+ var QUICK_ACTION_METADATA=[["\uEA73","Rename"],["\uEA82","Timeline"],["\uEBCC","Copy transcript"],["\uEBAC","Export"],["\uEAF5","Compact"],["\uEBEB","Switch session"],["\uEA60","New session"],["\uEB9E","Fork session"],["\uEBCC","Copy last response"],["\uEAA1","First message"],["\uEA9A","Last message"],["\uEA82","Last user message"],["\uEA9A","Next message"],["\uEAA1","Previous message"],["\uEBB3","Auto-approve permissions"]]
20
+ var QUICK_ACTIONS=QUICK_ACTION_IDS.map((command,index)=>({command,icon:QUICK_ACTION_METADATA[index][0],label:QUICK_ACTION_METADATA[index][1]
21
+ }))
22
+ function quickActionLabel(api,action){if(action.command!=="permission.mode")
23
+ return action.label
24
+ const command=api.keymap.getCommands?.({visibility:"registered"}).find((candidate)=>candidate.name===action.command)
25
+ return typeof command?.title==="string"?command.title:action.label
26
+ }
27
+ import {DialogSurface,SelectionBox,useDialogScroll,useDialogState,useDialogs,useIcons} from "./tui.js"
28
+ function QuickActionsDialog(props){const order=props.preferences.selectedQuickActionOrder
29
+ const dialogs=useDialogs(props.api)
30
+ const scroll=useDialogScroll()
31
+ const [selected,setSelected]=useDialogState("selection",order()[0])
32
+ const actions=createMemo(()=>order().map((id)=>({...QUICK_ACTIONS.find((action)=>action.command===id)
33
+ })))
34
+ const theme=()=>props.api.theme.current
35
+ const icons=useIcons()
36
+ const dimensions=useTerminalDimensions()
37
+ let body
38
+ function move(offset){const index=order().indexOf(selected())
39
+ setSelected(order()[(index + offset + order().length)%order().length])
40
+ }
41
+ const prefix=`${PLUGIN_ID}.quick-actions-settings`
42
+ createEffect(()=>{order()
43
+ const id=selected()
44
+ queueMicrotask(()=>{if(body&&!body.isDestroyed)
45
+ body.scrollChildIntoView(`${prefix}.${id}`)
46
+ })
47
+ })
48
+ const unregister=props.api.keymap.registerLayer({mode:"modal",priority:1000,commands:[{name:`${prefix}.previous`,run:()=>move(-1)
49
+ },{name:`${prefix}.next`,run:()=>move(1)
50
+ },{name:`${prefix}.toggle`,run:()=>props.preferences.toggleQuickAction(selected())
51
+ },{name:`${prefix}.up`,run:()=>props.preferences.moveQuickAction(selected(),-1)
52
+ },{name:`${prefix}.down`,run:()=>props.preferences.moveQuickAction(selected(),1)
53
+ },{name:`${prefix}.back`,run:dialogs.back
54
+ }],bindings:[{key:"up",cmd:`${prefix}.previous`
55
+ },{key:"down",cmd:`${prefix}.next`
56
+ },{key:"return",cmd:`${prefix}.toggle`
57
+ },{key:"space",cmd:`${prefix}.toggle`
58
+ },{key:"left",cmd:`${prefix}.up`
59
+ },{key:"right",cmd:`${prefix}.down`
60
+ },{key:"shift+up",cmd:`${prefix}.up`
61
+ },{key:"shift+down",cmd:`${prefix}.down`
62
+ },{key:"escape",cmd:`${prefix}.back`
63
+ }]
64
+ })
65
+ onCleanup2(unregister)
66
+ return _$createComponent(DialogSurface,{get api(){return props.api
67
+ },id:prefix,lift:true,get children(){return [(()=>{var _el$=_$createElement("box"),_el$2=_$createElement("text"),_el$3=_$createTextNode(` Quick actions`),_el$4=_$createElement("text"),_el$5=_$createTextNode(` Back (`),_el$6=_$createTextNode(`)`)
68
+ _$insertNode(_el$,_el$2)
69
+ _$insertNode(_el$,_el$4)
70
+ _$setProp(_el$,"flexDirection","row")
71
+ _$setProp(_el$,"justifyContent","space-between")
72
+ _$insertNode(_el$2,_el$3)
73
+ _$insert(_el$2,()=>icons.icon("actions"),_el$3)
74
+ _$insertNode(_el$4,_el$5)
75
+ _$insertNode(_el$4,_el$6)
76
+ _$setProp(_el$4,"onMouseUp",(event)=>{event.stopPropagation()
77
+ dialogs.back()
78
+ })
79
+ _$insert(_el$4,()=>icons.icon("left"),_el$5)
80
+ _$insert(_el$4,()=>icons.key("esc"),_el$6)
81
+ _$effect((_p$)=>{var _v$=TextAttributes.BOLD,_v$2=theme().text,_v$3=theme().textMuted
82
+ _v$!==_p$.e&&(_p$.e=_$setProp(_el$2,"attributes",_v$,_p$.e))
83
+ _v$2!==_p$.t&&(_p$.t=_$setProp(_el$2,"fg",_v$2,_p$.t))
84
+ _v$3!==_p$.a&&(_p$.a=_$setProp(_el$4,"fg",_v$3,_p$.a))
85
+ return _p$
86
+ },{e:undefined,t:undefined,a:undefined
87
+ })
88
+ return _el$
89
+ })(),(()=>{var _el$7=_$createElement("text"),_el$8=_$createTextNode(`Scope: `),_el$9=_$createTextNode(`. Changes save immediately.`)
90
+ _$insertNode(_el$7,_el$8)
91
+ _$insertNode(_el$7,_el$9)
92
+ _$setProp(_el$7,"wrapMode","word")
93
+ _$insert(_el$7,()=>props.preferences.preferenceScopeLabel(),_el$9)
94
+ _$effect((_$p)=>_$setProp(_el$7,"fg",theme().textMuted,_$p))
95
+ return _el$7
96
+ })(),(()=>{var _el$0=_$createElement("scrollbox"),_el$1=_$createElement("box")
97
+ _$insertNode(_el$0,_el$1)
98
+ _$use((node)=>{body=node
99
+ scroll.ref(node)
100
+ },_el$0)
101
+ _$setProp(_el$0,"scrollX",false)
102
+ _$setProp(_el$1,"gap",1)
103
+ _$insert(_el$1,_$createComponent(For,{get each(){return actions()
104
+ },children:(action,index)=>_$createComponent(SelectionBox,{get id(){return `${prefix}.${action.command}`
105
+ },flexDirection:"row",gap:1,paddingLeft:1,paddingRight:1,get backgroundColor(){return _$memo(()=>selected()===action.command)()?theme().backgroundElement:undefined
106
+ },onMouseOver:()=>setSelected(action.command),onMouseDown:()=>{setSelected(action.command)
107
+ props.preferences.toggleQuickAction(action.command)
108
+ },get children(){return [(()=>{var _el$16=_$createElement("text"),_el$17=_$createTextNode(` `),_el$18=_$createTextNode(`. `),_el$19=_$createTextNode(` `)
109
+ _$insertNode(_el$16,_el$17)
110
+ _$insertNode(_el$16,_el$18)
111
+ _$insertNode(_el$16,_el$19)
112
+ _$setProp(_el$16,"flexGrow",1)
113
+ _$insert(_el$16,()=>icons.icon(props.preferences.selectedQuickActionVisible(action.command)?"checked":"unchecked"),_el$17)
114
+ _$insert(_el$16,()=>index() + 1,_el$18)
115
+ _$insert(_el$16,()=>icons.action(action.command),_el$19)
116
+ _$insert(_el$16,()=>quickActionLabel(props.api,action),null)
117
+ _$effect((_$p)=>_$setProp(_el$16,"fg",theme().text,_$p))
118
+ return _el$16
119
+ })(),(()=>{var _el$20=_$createElement("text")
120
+ _$setProp(_el$20,"onMouseDown",(event)=>{event.stopPropagation()
121
+ setSelected(action.command)
122
+ props.preferences.moveQuickAction(action.command,-1)
123
+ })
124
+ _$insert(_el$20,()=>icons.icon("up"))
125
+ _$effect((_$p)=>_$setProp(_el$20,"fg",theme().accent,_$p))
126
+ return _el$20
127
+ })(),(()=>{var _el$21=_$createElement("text")
128
+ _$setProp(_el$21,"onMouseDown",(event)=>{event.stopPropagation()
129
+ setSelected(action.command)
130
+ props.preferences.moveQuickAction(action.command,1)
131
+ })
132
+ _$insert(_el$21,()=>icons.icon("down"))
133
+ _$effect((_$p)=>_$setProp(_el$21,"fg",theme().accent,_$p))
134
+ return _el$21
135
+ })()]
136
+ }})
137
+ }))
138
+ _$effect((_p$)=>{var{restore:_v$4,save:_v$5}=scroll,_v$6=Math.min(actions().length*2-1,Math.max(3,Math.floor(dimensions().height*0.75)-8))
139
+ _v$4!==_p$.e&&(_p$.e=_$setProp(_el$0,"renderBefore",_v$4,_p$.e))
140
+ _v$5!==_p$.t&&(_p$.t=_$setProp(_el$0,"renderAfter",_v$5,_p$.t))
141
+ _v$6!==_p$.a&&(_p$.a=_$setProp(_el$0,"height",_v$6,_p$.a))
142
+ return _p$
143
+ },{e:undefined,t:undefined,a:undefined
144
+ })
145
+ return _el$0
146
+ })(),(()=>{var _el$10=_$createElement("text"),_el$11=_$createTextNode(` navigate \xB7 `),_el$12=_$createTextNode(` toggle \xB7 `),_el$13=_$createTextNode(` or `),_el$15=_$createTextNode(` reorder`)
147
+ _$insertNode(_el$10,_el$11)
148
+ _$insertNode(_el$10,_el$12)
149
+ _$insertNode(_el$10,_el$13)
150
+ _$insertNode(_el$10,_el$15)
151
+ _$setProp(_el$10,"wrapMode","word")
152
+ _$insert(_el$10,()=>icons.key("up/down"),_el$11)
153
+ _$insert(_el$10,()=>icons.key("enter"),_el$12)
154
+ _$insert(_el$10,()=>icons.key("left/right"),_el$13)
155
+ _$insert(_el$10,()=>icons.key("shift+up/down"),_el$15)
156
+ _$effect((_$p)=>_$setProp(_el$10,"fg",theme().textMuted,_$p))
157
+ return _el$10
158
+ })()]
159
+ }})
160
+ }
161
+ export{QuickActionsDialog
162
+ }
@@ -0,0 +1,42 @@
1
+ import {PresetMenu,GroupAssignmentDialog
2
+ } from "./chunk-r37n7w97.js";
3
+ import {createComponent as _$createComponent} from "@opentui/solid"
4
+ import {useDialogs} from "./tui.js"
5
+ import {createEffect,createMemo,untrack} from "solid-js"
6
+ function createSkillGroupsModel(skills,preferences){const target=createMemo(()=>skills.target())
7
+ createEffect(()=>{const current=target()
8
+ untrack(()=>void skills.refresh(current).catch(()=>{}))
9
+ })
10
+ const options=createMemo(()=>{const live=new Map(skills.list(target()).map((skill)=>[skill.location,skill]))
11
+ const assigned=preferences.skillGroups?.()??{}
12
+ const entries=[...new Set([...live.keys(),...Object.keys(assigned)])].filter(Boolean).sort((left,right)=>{const label=(location)=>live.get(location)?.name??location.split("/").at(-2)??location
13
+ return label(left).localeCompare(label(right))||left.localeCompare(right)
14
+ }).map((location)=>({title:live.get(location)?.name??location.split("/").at(-2)??location,value:location,description:[assigned[location]??"Ungrouped",!live.has(location)&&"not in workspace",location].filter(Boolean).join(" \xB7 ")
15
+ }))
16
+ const state=skills.state(target())
17
+ if(state.error)
18
+ entries.push({title:"Retry loading skills",value:"__retry__",description:state.error.message})
19
+ if(entries.length===0)
20
+ entries.push({title:state.status==="loading"?"Loading skills\u2026":"No skills in this workspace",value:"__empty__",description:""
21
+ })
22
+ return entries
23
+ })
24
+ return{options,retry:()=>skills.refresh(target(),true)}}
25
+ function SkillGroupsDialog(props){const dialogs=useDialogs(props.api)
26
+ const model=createSkillGroupsModel(props.skills,props.preferences)
27
+ return _$createComponent(PresetMenu,{get api(){return props.api
28
+ },title:"Skill groups \xB7 user-wide",get options(){return model.options()
29
+ },onSelect:(option)=>{if(option.value==="__retry__"){model.retry().catch(()=>{})
30
+ return
31
+ }
32
+ if(option.value==="__empty__")
33
+ return
34
+ const location=option.value
35
+ dialogs.open(()=>_$createComponent(GroupAssignmentDialog,{get api(){return props.api
36
+ },get title(){return `Group for ${option.title}`
37
+ },assigned:()=>props.preferences.skillGroups?.()[location],groups:()=>[...new Set(Object.values(props.preferences.skillGroups?.()??{}))].sort((a,b)=>a.localeCompare(b)),onAssign:(group)=>props.preferences.setSkillGroup?.(location,group)
38
+ }))
39
+ }})
40
+ }
41
+ export{SkillGroupsDialog
42
+ }