@cat-factory/app 0.132.0 → 0.134.0

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.
@@ -5,6 +5,11 @@ import IconButton from '~/components/common/IconButton.vue'
5
5
 
6
6
  const ui = useUiStore()
7
7
  const board = useBoardStore()
8
+ // Toolbar contributions from the shared nav manifest (docs/initiatives/modular-vue-adoption.md,
9
+ // slice 1). First-party contributes none — this is the reactive extension point a consumer
10
+ // deployment uses to add a board-toolbar action via `registerAppModule`, gated + rendered like
11
+ // the sidebar/command entries with zero edits here.
12
+ const { toolbarItems, invoke: invokeNav } = useNavContributions()
8
13
  const execution = useExecutionStore()
9
14
  const workspace = useWorkspaceStore()
10
15
  const workspaceSettings = useWorkspaceSettingsStore()
@@ -233,6 +238,19 @@ const decisionItems = computed(() =>
233
238
  </UButton>
234
239
  </UDropdownMenu>
235
240
 
241
+ <!-- consumer-contributed toolbar actions from the nav manifest (none first-party) -->
242
+ <IconButton
243
+ v-for="item in toolbarItems"
244
+ :key="item.id"
245
+ :label="t(item.labelKey)"
246
+ :icon="item.icon"
247
+ color="neutral"
248
+ variant="ghost"
249
+ size="sm"
250
+ :data-testid="item.testId"
251
+ @click="invokeNav(item)"
252
+ />
253
+
236
254
  <!-- human-actionable notifications (merge review, pipeline complete, CI failed) -->
237
255
  <NotificationsInbox />
238
256
 
@@ -25,6 +25,13 @@ const tasks = useTasksStore()
25
25
  const library = useFragmentLibraryStore()
26
26
  const access = useWorkspaceAccess()
27
27
 
28
+ // The static destination catalog + its RBAC/availability gating now comes from
29
+ // the shared nav manifest (docs/initiatives/modular-vue-adoption.md, slice 1),
30
+ // rendered here as command entries. The DYNAMIC per-connection commands below
31
+ // (github/slack/doc/task connect + import) stay local to the palette — they vary
32
+ // per live connection, so they are not part of the static manifest this slice.
33
+ const { commandGroups, invoke } = useNavContributions()
34
+
28
35
  const open = computed({
29
36
  get: () => ui.commandBarOpen,
30
37
  set: (v: boolean) => (v ? ui.openCommandBar() : ui.closeCommandBar()),
@@ -33,211 +40,117 @@ const open = computed({
33
40
  const query = ref('')
34
41
  const activeIndex = ref(0)
35
42
 
36
- const commands = computed<Command[]>(() => {
37
- const list: Command[] = []
38
-
39
- const groupCreate = t('layout.commandBar.groups.create')
40
- const groupRepositories = t('layout.commandBar.groups.repositories')
43
+ // The per-connection integration commands the manifest deliberately doesn't
44
+ // carry: their label (connect vs manage) and set (one per document/task source)
45
+ // depend on live connection state. Gated by `integrations.manage`, they render
46
+ // under the palette's Integrations group.
47
+ const dynamicIntegrationCommands = computed<Command[]>(() => {
48
+ if (!access.canManageIntegrations.value) return []
41
49
  const groupIntegrations = t('layout.commandBar.groups.integrations')
42
- const groupWorkspace = t('layout.commandBar.groups.workspace')
43
- const groupAccount = t('layout.commandBar.groups.account')
44
-
45
- // ---- Create -------------------------------------------------------------
46
- // Command entries mirror the SideBar nav gating: each is listed only when the caller
47
- // holds the permission its action's writes require (dev-open ⇒ every `can*` is true).
48
- if (access.canWriteBoard.value) {
49
- list.push({
50
- id: 'new-pipeline',
51
- label: t('layout.commandBar.cmd.newPipeline'),
52
- group: groupCreate,
53
- icon: 'i-lucide-workflow',
54
- keywords: t('layout.commandBar.keywords.newPipeline'),
55
- run: () => ui.openBuilder(),
56
- })
57
- }
58
-
59
- // ---- Repositories -------------------------------------------------------
60
- if (github.available && access.canWriteBoard.value) {
50
+ const list: Command[] = []
51
+ if (github.available) {
61
52
  list.push({
62
- id: 'add-from-repo',
63
- label: t('layout.commandBar.cmd.addFromRepo'),
64
- group: groupRepositories,
65
- icon: 'i-lucide-folder-git-2',
66
- keywords: t('layout.commandBar.keywords.addFromRepo'),
67
- run: () => ui.openAddService(),
53
+ id: 'github',
54
+ label: github.connected
55
+ ? t('layout.commandBar.cmd.githubManage')
56
+ : t('layout.commandBar.cmd.githubConnect'),
57
+ group: groupIntegrations,
58
+ icon: 'i-lucide-github',
59
+ keywords: t('layout.commandBar.keywords.github'),
60
+ run: () => ui.openGitHub(),
68
61
  })
69
62
  }
70
- if (access.canManageIntegrations.value) {
63
+ if (slack.available) {
71
64
  list.push({
72
- id: 'bootstrap-repo',
73
- label: t('layout.commandBar.cmd.bootstrapRepo'),
74
- group: groupRepositories,
75
- icon: 'i-lucide-git-branch-plus',
76
- keywords: t('layout.commandBar.keywords.bootstrapRepo'),
77
- run: () => ui.openBootstrap(),
65
+ id: 'slack',
66
+ label: slack.connected
67
+ ? t('layout.commandBar.cmd.slackManage')
68
+ : t('layout.commandBar.cmd.slackConnect'),
69
+ group: groupIntegrations,
70
+ icon: 'i-lucide-slack',
71
+ keywords: t('layout.commandBar.keywords.slack'),
72
+ run: () => ui.openSlack(),
78
73
  })
79
74
  }
80
-
81
- // ---- Integrations (connection management `integrations.manage`) --------
82
- if (access.canManageIntegrations.value) {
83
- if (github.available) {
75
+ if (documents.available) {
76
+ for (const src of documents.sources) {
84
77
  list.push({
85
- id: 'github',
86
- label: github.connected
87
- ? t('layout.commandBar.cmd.githubManage')
88
- : t('layout.commandBar.cmd.githubConnect'),
78
+ id: `doc-connect-${src.source}`,
79
+ label: documents.isConnected(src.source)
80
+ ? t('layout.commandBar.cmd.sourceManage', { source: src.label })
81
+ : t('layout.commandBar.cmd.sourceConnect', { source: src.label }),
89
82
  group: groupIntegrations,
90
- icon: 'i-lucide-github',
91
- keywords: t('layout.commandBar.keywords.github'),
92
- run: () => ui.openGitHub(),
83
+ icon: src.icon,
84
+ keywords: t('layout.commandBar.keywords.documentSource'),
85
+ run: () => ui.openDocumentConnect(src.source),
93
86
  })
94
87
  }
95
- if (slack.available) {
88
+ if (documents.anyConnected) {
96
89
  list.push({
97
- id: 'slack',
98
- label: slack.connected
99
- ? t('layout.commandBar.cmd.slackManage')
100
- : t('layout.commandBar.cmd.slackConnect'),
90
+ id: 'doc-import',
91
+ label: t('layout.commandBar.cmd.documentImport'),
101
92
  group: groupIntegrations,
102
- icon: 'i-lucide-slack',
103
- keywords: t('layout.commandBar.keywords.slack'),
104
- run: () => ui.openSlack(),
93
+ icon: 'i-lucide-file-down',
94
+ keywords: t('layout.commandBar.keywords.documentImport'),
95
+ run: () => ui.openDocumentImport(null),
105
96
  })
106
97
  }
107
- if (documents.available) {
108
- for (const src of documents.sources) {
109
- list.push({
110
- id: `doc-connect-${src.source}`,
111
- label: documents.isConnected(src.source)
112
- ? t('layout.commandBar.cmd.sourceManage', { source: src.label })
113
- : t('layout.commandBar.cmd.sourceConnect', { source: src.label }),
114
- group: groupIntegrations,
115
- icon: src.icon,
116
- keywords: t('layout.commandBar.keywords.documentSource'),
117
- run: () => ui.openDocumentConnect(src.source),
118
- })
119
- }
120
- if (documents.anyConnected) {
121
- list.push({
122
- id: 'doc-import',
123
- label: t('layout.commandBar.cmd.documentImport'),
124
- group: groupIntegrations,
125
- icon: 'i-lucide-file-down',
126
- keywords: t('layout.commandBar.keywords.documentImport'),
127
- run: () => ui.openDocumentImport(null),
128
- })
129
- }
130
- }
131
- if (tasks.available) {
132
- for (const src of tasks.sources) {
133
- list.push({
134
- id: `task-connect-${src.source}`,
135
- label: src.available
136
- ? t('layout.commandBar.cmd.sourceManage', { source: src.label })
137
- : t('layout.commandBar.cmd.sourceConnect', { source: src.label }),
138
- group: groupIntegrations,
139
- icon: src.icon,
140
- keywords: t('layout.commandBar.keywords.taskSource'),
141
- run: () => ui.openTaskConnect(src.source),
142
- })
143
- }
144
- if (tasks.anyOffered) {
145
- list.push({
146
- id: 'task-import',
147
- label: t('layout.commandBar.cmd.taskImport'),
148
- group: groupIntegrations,
149
- icon: 'i-lucide-file-down',
150
- keywords: t('layout.commandBar.keywords.taskImport'),
151
- run: () => ui.openTaskImport(null),
152
- })
153
- }
154
- }
155
98
  }
156
-
157
- // ---- Workspace ----------------------------------------------------------
158
- // Workspace + model configuration and the fragment library are `settings.manage`.
159
- if (access.canManageSettings.value) {
160
- if (library.available) {
99
+ if (tasks.available) {
100
+ for (const src of tasks.sources) {
161
101
  list.push({
162
- id: 'fragments',
163
- label: t('layout.commandBar.cmd.fragments'),
164
- group: groupWorkspace,
165
- icon: 'i-lucide-book-marked',
166
- keywords: t('layout.commandBar.keywords.fragments'),
167
- run: () => ui.openFragmentLibrary(),
102
+ id: `task-connect-${src.source}`,
103
+ label: src.available
104
+ ? t('layout.commandBar.cmd.sourceManage', { source: src.label })
105
+ : t('layout.commandBar.cmd.sourceConnect', { source: src.label }),
106
+ group: groupIntegrations,
107
+ icon: src.icon,
108
+ keywords: t('layout.commandBar.keywords.taskSource'),
109
+ run: () => ui.openTaskConnect(src.source),
110
+ })
111
+ }
112
+ if (tasks.anyOffered) {
113
+ list.push({
114
+ id: 'task-import',
115
+ label: t('layout.commandBar.cmd.taskImport'),
116
+ group: groupIntegrations,
117
+ icon: 'i-lucide-file-down',
118
+ keywords: t('layout.commandBar.keywords.taskImport'),
119
+ run: () => ui.openTaskImport(null),
168
120
  })
169
121
  }
170
- list.push({
171
- id: 'merge-thresholds',
172
- label: t('layout.commandBar.cmd.mergeThresholds'),
173
- group: groupWorkspace,
174
- icon: 'i-lucide-git-merge',
175
- keywords: t('layout.commandBar.keywords.mergeThresholds'),
176
- run: () => ui.openWorkspaceSettings('merge'),
177
- })
178
- list.push({
179
- id: 'workspace-settings',
180
- label: t('layout.commandBar.cmd.workspaceSettings'),
181
- group: groupWorkspace,
182
- icon: 'i-lucide-sliders-horizontal',
183
- keywords: t('layout.commandBar.keywords.workspaceSettings'),
184
- run: () => ui.openWorkspaceSettings(),
185
- })
186
- list.push({
187
- id: 'model-configuration',
188
- label: t('layout.commandBar.cmd.modelConfiguration'),
189
- group: groupWorkspace,
190
- icon: 'i-lucide-cpu',
191
- keywords: t('layout.commandBar.keywords.modelConfiguration'),
192
- run: () => ui.openModelConfig(),
193
- })
194
- list.push({
195
- id: 'service-fragment-defaults',
196
- label: t('layout.commandBar.cmd.serviceFragmentDefaults'),
197
- group: groupWorkspace,
198
- icon: 'i-lucide-book-open-check',
199
- keywords: t('layout.commandBar.keywords.serviceFragmentDefaults'),
200
- run: () => ui.openWorkspaceSettings('fragments'),
201
- })
202
- }
203
- list.push({
204
- id: 'account-settings',
205
- label: t('layout.commandBar.cmd.accountSettings'),
206
- group: groupAccount,
207
- icon: 'i-lucide-settings',
208
- keywords: t('layout.commandBar.keywords.accountSettings'),
209
- run: () => ui.openAccountSettings(),
210
- })
211
- list.push({
212
- id: 'local-models',
213
- label: t('layout.commandBar.cmd.localModels'),
214
- group: groupWorkspace,
215
- icon: 'i-lucide-server',
216
- keywords: t('layout.commandBar.keywords.localModels'),
217
- run: () => ui.openLocalModels(),
218
- })
219
- if (access.canManageIntegrations.value) {
220
- list.push({
221
- id: 'sandbox',
222
- label: t('layout.commandBar.cmd.sandbox'),
223
- group: groupWorkspace,
224
- icon: 'i-lucide-flask-conical',
225
- keywords: t('layout.commandBar.keywords.sandbox'),
226
- run: () => ui.openSandbox(),
227
- })
228
122
  }
229
- list.push({
230
- id: 'keyboard-shortcuts',
231
- label: t('layout.commandBar.cmd.shortcuts'),
232
- group: groupWorkspace,
233
- icon: 'i-lucide-keyboard',
234
- keywords: t('layout.commandBar.keywords.shortcuts'),
235
- run: () => ui.openShortcutsHelp(),
236
- })
237
-
238
123
  return list
239
124
  })
240
125
 
126
+ const commands = computed<Command[]>(() => {
127
+ // Flatten the reactively-gated manifest command groups (already in canonical
128
+ // order: create, repositories, integrations, workspace, account), splicing the
129
+ // dynamic per-connection commands into the Integrations group's position.
130
+ const staticByGroup = new Map(commandGroups.value.map((g) => [g.group, g]))
131
+ const asCommand = (g: (typeof commandGroups.value)[number]): Command[] =>
132
+ g.items.map((ci) => ({
133
+ id: ci.item.id,
134
+ label: t(ci.labelKey),
135
+ group: t(g.labelKey),
136
+ icon: ci.item.icon,
137
+ keywords: ci.keywordsKey ? t(ci.keywordsKey) : undefined,
138
+ run: () => invoke(ci.item),
139
+ }))
140
+ const groupOrEmpty = (name: (typeof commandGroups.value)[number]['group']) => {
141
+ const g = staticByGroup.get(name)
142
+ return g ? asCommand(g) : []
143
+ }
144
+ return [
145
+ ...groupOrEmpty('create'),
146
+ ...groupOrEmpty('repositories'),
147
+ ...groupOrEmpty('integrations'),
148
+ ...dynamicIntegrationCommands.value,
149
+ ...groupOrEmpty('workspace'),
150
+ ...groupOrEmpty('account'),
151
+ ]
152
+ })
153
+
241
154
  const filtered = computed<Command[]>(() => {
242
155
  const q = query.value.trim().toLowerCase()
243
156
  if (!q) return commands.value
@@ -19,53 +19,16 @@ const github = useGitHubStore()
19
19
  const slack = useSlackStore()
20
20
  const library = useFragmentLibraryStore()
21
21
  const workspace = useWorkspaceStore()
22
- const accounts = useAccountsStore()
23
- const auth = useAuthStore()
24
22
  const providerConnections = useProviderConnectionsStore()
25
23
  const ui = useUiStore()
26
- const access = useWorkspaceAccess()
27
24
 
28
- // The operator dashboard (deployment-level run health) is sensitive cross-workspace data,
29
- // so it's shown only to an admin of the active account (matching the backend admin gate).
30
- const isAccountAdmin = computed(() => accounts.activeAccount?.roles?.includes('admin') ?? false)
31
-
32
- // Workspace-RBAC nav gating: each management destination is shown only when the caller
33
- // holds the permission its writes require (the SPA mirror of the admin-tier controller
34
- // middleware a member/viewer who'd only get a 403 never sees the entry). Board authoring
35
- // (create/repos) needs `board.write`; connections/infra/sandbox/bootstrap need
36
- // `integrations.manage`; workspace + model configuration and the fragment library need
37
- // `settings.manage`. Kaizen and account settings are reads / account-scoped and stay.
38
- // Absent access (dev-open) ⇒ every `can*` is true, so nothing is hidden.
39
- const showCreate = computed(() => access.canWriteBoard.value)
40
- const showAddFromRepo = computed(() => github.available && access.canWriteBoard.value)
41
- const showBootstrap = computed(() => access.canManageIntegrations.value)
42
- const showRepositories = computed(() => showAddFromRepo.value || showBootstrap.value)
43
- const showIntegrationsHub = computed(() => access.canManageIntegrations.value)
44
- const showSandbox = computed(() => access.canManageIntegrations.value)
45
- // The Configuration section carries workspace/model settings (`settings.manage`) AND the
46
- // account-scoped account-settings / operator-dashboard entries, so it shows when the caller
47
- // can manage settings OR accounts are enabled (the account entries have their own gates).
48
- const showConfiguration = computed(() => access.canManageSettings.value || accounts.enabled)
49
-
50
- // The Infrastructure menu (agent-container execution + test environments) shows whenever the
51
- // deployment reports its infrastructure capability — every facade populates `auth.infrastructure`
52
- // (it drives the execution-backend selector), so there is always an execution + test-env backend
53
- // to view, even on a Worker/Node deployment with no runner-pool/environment connection registered.
54
- // The old provider-availability/local-mode signals stay as a defensive fallback for a backend that
55
- // (somehow) omits the descriptor.
56
- const showInfrastructure = computed(
57
- () =>
58
- // Provisioning/managing infrastructure is `integrations.manage`, so a member/viewer
59
- // never sees the section (they'd only 403 on the writes inside it).
60
- access.canManageIntegrations.value &&
61
- (auth.infrastructure != null ||
62
- auth.localMode?.enabled === true ||
63
- providerConnections.isAvailable('runner-pool') ||
64
- providerConnections.isAvailable('environment')),
65
- )
66
-
67
- // The prompt-fragment library is a `settings.manage` authoring surface.
68
- const showWorkspaceContext = computed(() => library.available && access.canManageSettings.value)
25
+ // The nav catalog + its reactive RBAC/availability gating now lives in the shared
26
+ // modular-vue manifest (docs/initiatives/modular-vue-adoption.md, slice 1): every
27
+ // destination is declared once in `nav-contributions.ts`, gated by `navSlotFilter`
28
+ // over a reactive `gates` service, and rendered here (and in CommandBar / BoardToolbar)
29
+ // from `useReactiveSlots`. Sections + items appear/disappear reactively as a permission
30
+ // or connection flips, so this shell no longer hand-rolls per-item `show*` computeds.
31
+ const { sidebarGroups, invoke } = useNavContributions()
69
32
 
70
33
  // `isCompact` (< lg) is the breakpoint at which the navbar is an off-canvas drawer;
71
34
  // above it the aside is static and the drawer flag is inert.
@@ -191,240 +154,29 @@ watch(
191
154
  <UKbd value="⌘K" />
192
155
  </button>
193
156
 
194
- <template v-if="showCreate">
195
- <USeparator />
196
- <section>
197
- <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
198
- {{ t('nav.create') }}
199
- </h2>
200
- <div class="space-y-1.5">
201
- <UButton
202
- block
203
- color="primary"
204
- variant="soft"
205
- size="sm"
206
- icon="i-lucide-workflow"
207
- class="justify-start"
208
- data-testid="nav-build-pipeline"
209
- @click="ui.openBuilder()"
210
- >
211
- {{ t('nav.buildPipeline') }}
212
- </UButton>
213
- </div>
214
- </section>
215
- </template>
216
-
217
- <template v-if="showRepositories">
218
- <USeparator />
219
- <section>
220
- <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
221
- {{ t('nav.repositories') }}
222
- </h2>
223
- <div class="space-y-1.5">
224
- <UButton
225
- v-if="showAddFromRepo"
226
- block
227
- color="primary"
228
- variant="soft"
229
- size="sm"
230
- icon="i-lucide-folder-git-2"
231
- class="justify-start"
232
- data-testid="nav-add-from-repo"
233
- @click="ui.openAddService()"
234
- >
235
- {{ t('nav.addFromRepo') }}
236
- </UButton>
237
- <UButton
238
- v-if="showBootstrap"
239
- block
240
- color="primary"
241
- variant="soft"
242
- size="sm"
243
- icon="i-lucide-git-branch-plus"
244
- class="justify-start"
245
- data-testid="nav-bootstrap-repo"
246
- @click="ui.openBootstrap()"
247
- >
248
- {{ t('nav.bootstrapRepo') }}
249
- </UButton>
250
- </div>
251
- </section>
252
- </template>
253
-
254
- <USeparator />
255
-
256
- <section>
257
- <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
258
- {{ t('nav.integrations') }}
259
- </h2>
260
- <div class="space-y-1.5">
261
- <!-- Every external system the workspace can enable/link now lives behind
262
- this single button — the hub modal lists them grouped (source control,
263
- communication, documents, trackers, observability, model providers). -->
264
- <UButton
265
- v-if="showIntegrationsHub"
266
- block
267
- color="primary"
268
- variant="soft"
269
- size="sm"
270
- icon="i-lucide-blocks"
271
- class="justify-start"
272
- data-testid="nav-integrations"
273
- @click="ui.openIntegrations()"
274
- >
275
- {{ t('nav.integrations') }}
276
- </UButton>
277
- <!-- The Sandbox: try prompt versions/models against graded fixtures, off to the
278
- side of the board. Opens the on-demand testing window. -->
279
- <UButton
280
- v-if="showSandbox"
281
- block
282
- color="primary"
283
- variant="soft"
284
- size="sm"
285
- icon="i-lucide-flask-conical"
286
- class="justify-start"
287
- @click="ui.openSandbox()"
288
- >
289
- {{ t('nav.sandbox') }}
290
- </UButton>
291
- <!-- The Kaizen screen: grading history + verified prompt/agent/model combos. A
292
- read surface (grading history), so it stays visible to every resolved role. -->
293
- <UButton
294
- block
295
- color="primary"
296
- variant="soft"
297
- size="sm"
298
- icon="i-lucide-sparkles"
299
- class="justify-start"
300
- @click="ui.openKaizen()"
301
- >
302
- {{ t('nav.kaizen') }}
303
- </UButton>
304
- </div>
305
- </section>
306
-
307
- <template v-if="showInfrastructure">
308
- <USeparator />
309
- <section>
310
- <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
311
- {{ t('nav.infrastructure') }}
312
- </h2>
313
- <div class="space-y-1.5">
314
- <!-- Where agent containers run + Tester environments + (local mode) the warm
315
- pool/checkout reuse. Its own top-level destination, no longer inside the
316
- Integrations hub. -->
317
- <UButton
318
- block
319
- color="primary"
320
- variant="soft"
321
- size="sm"
322
- icon="i-lucide-server-cog"
323
- class="justify-start"
324
- data-testid="nav-infrastructure"
325
- @click="ui.openInfrastructure()"
326
- >
327
- {{ t('nav.infrastructure') }}
328
- </UButton>
329
- <!-- Guided detect → review → preflight → save flow for a service frame's
330
- docker-compose provisioning (so the single Deployer stands its env up). -->
331
- <UButton
332
- block
333
- color="primary"
334
- variant="soft"
335
- size="sm"
336
- icon="i-lucide-flask-conical"
337
- class="justify-start"
338
- data-testid="nav-environment-setup"
339
- @click="ui.openEnvironmentSetup()"
340
- >
341
- {{ t('nav.environmentSetup') }}
342
- </UButton>
343
- </div>
344
- </section>
345
- </template>
346
-
347
- <template v-if="showWorkspaceContext">
348
- <USeparator />
349
- <section>
350
- <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
351
- {{ t('nav.workspaceContext') }}
352
- </h2>
353
- <UButton
354
- block
355
- color="primary"
356
- variant="soft"
357
- size="sm"
358
- icon="i-lucide-book-marked"
359
- class="justify-start"
360
- @click="ui.openFragmentLibrary()"
361
- >
362
- {{ t('nav.contextFragments') }}
363
- </UButton>
364
- </section>
365
- </template>
366
-
367
- <template v-if="showConfiguration">
157
+ <!-- Sections + items come from the shared nav manifest, already gated by the
158
+ reactive slotFilter (docs/initiatives/modular-vue-adoption.md, slice 1). An
159
+ empty section is dropped upstream, so there is no per-section `v-if` here. -->
160
+ <template v-for="section in sidebarGroups" :key="section.group">
368
161
  <USeparator />
369
162
  <section>
370
163
  <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
371
- {{ t('nav.configuration') }}
164
+ {{ t(section.labelKey) }}
372
165
  </h2>
373
166
  <div class="space-y-1.5">
374
- <!-- Merge thresholds, issue writeback and default service best practices are
375
- now tabs inside Workspace settings. `settings.manage` — hidden for members/viewers. -->
376
- <UButton
377
- v-if="access.canManageSettings.value"
378
- block
379
- color="primary"
380
- variant="soft"
381
- size="sm"
382
- icon="i-lucide-sliders-horizontal"
383
- class="justify-start"
384
- data-testid="nav-workspace-settings"
385
- @click="ui.openWorkspaceSettings()"
386
- >
387
- {{ t('nav.workspaceSettings') }}
388
- </UButton>
389
- <UButton
390
- v-if="access.canManageSettings.value"
391
- block
392
- color="primary"
393
- variant="soft"
394
- size="sm"
395
- icon="i-lucide-cpu"
396
- class="justify-start"
397
- @click="ui.openModelConfig()"
398
- >
399
- {{ t('nav.modelConfiguration') }}
400
- </UButton>
401
- <!-- Account & team: members + roles, invitations, email sender, account API keys.
402
- Shown once accounts (auth) are enabled. -->
403
- <UButton
404
- v-if="accounts.enabled"
405
- block
406
- color="primary"
407
- variant="soft"
408
- size="sm"
409
- icon="i-lucide-users"
410
- class="justify-start"
411
- @click="ui.openAccountSettings()"
412
- >
413
- {{ t('nav.accountSettings') }}
414
- </UButton>
415
- <!-- Platform observability: deployment-level run health. Admin-only, like the backend gate. -->
416
167
  <UButton
417
- v-if="accounts.enabled && isAccountAdmin"
168
+ v-for="item in section.items"
169
+ :key="item.id"
418
170
  block
419
171
  color="primary"
420
172
  variant="soft"
421
173
  size="sm"
422
- icon="i-lucide-gauge"
174
+ :icon="item.icon"
423
175
  class="justify-start"
424
- data-testid="nav-operator-dashboard"
425
- @click="ui.openOperatorDashboard()"
176
+ :data-testid="item.testId"
177
+ @click="invoke(item)"
426
178
  >
427
- {{ t('nav.operatorDashboard') }}
179
+ {{ t(item.labelKey) }}
428
180
  </UButton>
429
181
  </div>
430
182
  </section>