@agent-native/dispatch 0.13.13 → 0.14.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.
Files changed (74) hide show
  1. package/dist/actions/delete-destination.d.ts +12 -12
  2. package/dist/actions/navigate.js +1 -1
  3. package/dist/actions/navigate.js.map +1 -1
  4. package/dist/actions/provider-api-audit.d.ts +7 -0
  5. package/dist/actions/provider-api-audit.d.ts.map +1 -0
  6. package/dist/actions/provider-api-audit.js +74 -0
  7. package/dist/actions/provider-api-audit.js.map +1 -0
  8. package/dist/actions/provider-api-catalog.d.ts +1 -1
  9. package/dist/actions/provider-api-request.js +10 -0
  10. package/dist/actions/provider-api-request.js.map +1 -1
  11. package/dist/actions/send-platform-message.d.ts +1 -0
  12. package/dist/actions/send-platform-message.js +43 -22
  13. package/dist/actions/send-platform-message.js.map +1 -1
  14. package/dist/actions/view-screen.js +9 -0
  15. package/dist/actions/view-screen.js.map +1 -1
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +75 -70
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  20. package/dist/components/messaging-setup-panel.js +203 -127
  21. package/dist/components/messaging-setup-panel.js.map +1 -1
  22. package/dist/routes/index.d.ts.map +1 -1
  23. package/dist/routes/index.js +1 -0
  24. package/dist/routes/index.js.map +1 -1
  25. package/dist/routes/pages/messaging.js +1 -1
  26. package/dist/routes/pages/messaging.js.map +1 -1
  27. package/dist/routes/pages/operations.d.ts +5 -0
  28. package/dist/routes/pages/operations.d.ts.map +1 -0
  29. package/dist/routes/pages/operations.js +51 -0
  30. package/dist/routes/pages/operations.js.map +1 -0
  31. package/dist/routes/pages/overview.d.ts +2 -2
  32. package/dist/server/lib/dispatch-integrations.d.ts +7 -1
  33. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  34. package/dist/server/lib/dispatch-integrations.js +178 -8
  35. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  36. package/dist/server/lib/dispatch-routing.d.ts +6 -0
  37. package/dist/server/lib/dispatch-routing.d.ts.map +1 -0
  38. package/dist/server/lib/dispatch-routing.js +30 -0
  39. package/dist/server/lib/dispatch-routing.js.map +1 -0
  40. package/dist/server/lib/env-config.d.ts.map +1 -1
  41. package/dist/server/lib/env-config.js +50 -0
  42. package/dist/server/lib/env-config.js.map +1 -1
  43. package/dist/server/lib/onboarding-steps.d.ts.map +1 -1
  44. package/dist/server/lib/onboarding-steps.js +25 -0
  45. package/dist/server/lib/onboarding-steps.js.map +1 -1
  46. package/dist/server/lib/provider-api.d.ts +2 -2
  47. package/dist/server/lib/provider-api.d.ts.map +1 -1
  48. package/dist/server/plugins/integrations.d.ts.map +1 -1
  49. package/dist/server/plugins/integrations.js +5 -2
  50. package/dist/server/plugins/integrations.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/actions/navigate.ts +1 -1
  53. package/src/actions/provider-api-audit.spec.ts +52 -0
  54. package/src/actions/provider-api-audit.ts +88 -0
  55. package/src/actions/provider-api-request.ts +10 -0
  56. package/src/actions/send-platform-message.spec.ts +108 -0
  57. package/src/actions/send-platform-message.ts +73 -25
  58. package/src/actions/view-screen.ts +11 -0
  59. package/src/components/layout/Layout.spec.tsx +154 -0
  60. package/src/components/layout/Layout.tsx +196 -189
  61. package/src/components/messaging-setup-panel.spec.tsx +191 -0
  62. package/src/components/messaging-setup-panel.tsx +472 -193
  63. package/src/routes/index.spec.ts +23 -0
  64. package/src/routes/index.ts +1 -0
  65. package/src/routes/pages/messaging.tsx +1 -1
  66. package/src/routes/pages/operations.tsx +134 -0
  67. package/src/server/lib/dispatch-integrations.spec.ts +113 -0
  68. package/src/server/lib/dispatch-integrations.ts +239 -6
  69. package/src/server/lib/dispatch-routing.spec.ts +44 -0
  70. package/src/server/lib/dispatch-routing.ts +42 -0
  71. package/src/server/lib/env-config.ts +50 -0
  72. package/src/server/lib/onboarding-steps.ts +32 -0
  73. package/src/server/plugins/integrations.ts +5 -2
  74. package/src/styles/dispatch-css.spec.ts +11 -0
@@ -16,6 +16,7 @@ import {
16
16
  import { ExtensionsSidebarSection } from "@agent-native/core/client/extensions";
17
17
  import { InvitationBanner, OrgSwitcher } from "@agent-native/core/client/org";
18
18
  import {
19
+ IconActivity,
19
20
  IconArrowUpRight,
20
21
  IconApps,
21
22
  IconBrain,
@@ -60,6 +61,7 @@ import {
60
61
  } from "../ui/dropdown-menu";
61
62
  import { Input } from "../ui/input";
62
63
  import { Sheet, SheetContent, SheetDescription, SheetTitle } from "../ui/sheet";
64
+ import { Skeleton } from "../ui/skeleton";
63
65
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
64
66
  import { Header } from "./Header";
65
67
  import { HeaderActionsProvider } from "./HeaderActions";
@@ -113,37 +115,65 @@ const PRIMARY_NAV_ITEMS = [
113
115
  icon: IconApps,
114
116
  section: "primary",
115
117
  },
118
+ ] as const satisfies readonly DispatchNavItem[];
119
+
120
+ const OPERATIONS_NAV_ITEMS = [
121
+ {
122
+ id: "operations",
123
+ to: "/operations",
124
+ label: "Operations",
125
+ icon: IconActivity,
126
+ section: "operations",
127
+ },
116
128
  {
117
129
  id: "metrics",
118
130
  to: "/metrics",
119
131
  label: "Metrics",
120
132
  icon: IconChartBar,
121
- section: "primary",
133
+ section: "operations",
122
134
  },
123
135
  {
124
- id: "vault",
125
- to: "/vault",
126
- label: "Vault",
127
- icon: IconKey,
128
- section: "primary",
136
+ id: "automations",
137
+ to: "/automations",
138
+ label: "Automations",
139
+ icon: IconSettingsAutomation,
140
+ section: "operations",
141
+ },
142
+ {
143
+ id: "approvals",
144
+ to: "/approvals",
145
+ label: "Approvals",
146
+ icon: IconShieldCheck,
147
+ section: "operations",
148
+ },
149
+ {
150
+ id: "destinations",
151
+ to: "/destinations",
152
+ label: "Destinations",
153
+ icon: IconArrowUpRight,
154
+ section: "operations",
129
155
  },
130
156
  {
131
157
  id: "integrations",
132
158
  to: "/integrations",
133
159
  label: "Integrations",
134
160
  icon: IconPuzzle,
135
- section: "primary",
161
+ section: "operations",
162
+ },
163
+ {
164
+ id: "vault",
165
+ to: "/vault",
166
+ label: "Vault",
167
+ icon: IconKey,
168
+ section: "operations",
136
169
  },
137
170
  {
138
171
  id: "agents",
139
172
  to: "/agents",
140
173
  label: "Agents",
141
174
  icon: IconPlugConnected,
142
- section: "primary",
175
+ section: "operations",
143
176
  },
144
- ] as const satisfies readonly DispatchNavItem[];
145
-
146
- const OPERATIONS_NAV_ITEMS = [
147
177
  {
148
178
  id: "workspace",
149
179
  to: "/workspace",
@@ -151,6 +181,16 @@ const OPERATIONS_NAV_ITEMS = [
151
181
  icon: IconLayersSubtract,
152
182
  section: "operations",
153
183
  },
184
+ {
185
+ id: "settings",
186
+ to: "/settings",
187
+ label: "Settings",
188
+ icon: IconSettings,
189
+ section: "operations",
190
+ },
191
+ ] as const satisfies readonly DispatchNavItem[];
192
+
193
+ const ADVANCED_NAV_ITEMS = [
154
194
  {
155
195
  id: "messaging",
156
196
  to: "/messaging",
@@ -158,13 +198,6 @@ const OPERATIONS_NAV_ITEMS = [
158
198
  icon: IconBrandTelegram,
159
199
  section: "operations",
160
200
  },
161
- {
162
- id: "destinations",
163
- to: "/destinations",
164
- label: "Destinations",
165
- icon: IconArrowUpRight,
166
- section: "operations",
167
- },
168
201
  {
169
202
  id: "identities",
170
203
  to: "/identities",
@@ -172,20 +205,6 @@ const OPERATIONS_NAV_ITEMS = [
172
205
  icon: IconFingerprint,
173
206
  section: "operations",
174
207
  },
175
- {
176
- id: "approvals",
177
- to: "/approvals",
178
- label: "Approvals",
179
- icon: IconShieldCheck,
180
- section: "operations",
181
- },
182
- {
183
- id: "automations",
184
- to: "/automations",
185
- label: "Automations",
186
- icon: IconSettingsAutomation,
187
- section: "operations",
188
- },
189
208
  {
190
209
  id: "audit",
191
210
  to: "/audit",
@@ -207,13 +226,6 @@ const OPERATIONS_NAV_ITEMS = [
207
226
  icon: IconMessages,
208
227
  section: "operations",
209
228
  },
210
- {
211
- id: "settings",
212
- to: "/settings",
213
- label: "Settings",
214
- icon: IconSettings,
215
- section: "operations",
216
- },
217
229
  ] as const satisfies readonly DispatchNavItem[];
218
230
 
219
231
  const EMPTY_NAV_ITEMS: readonly DispatchNavItem[] = [];
@@ -338,6 +350,7 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
338
350
  const {
339
351
  threads,
340
352
  activeThreadId,
353
+ isLoading: chatsLoading,
341
354
  createThread,
342
355
  switchThread,
343
356
  renameThread,
@@ -439,134 +452,123 @@ function DispatchChatsSection({ onNavigate }: { onNavigate?: () => void }) {
439
452
  }
440
453
 
441
454
  return (
442
- <div className="mt-2 border-s border-sidebar-border/70 ps-3">
443
- <div className="mb-1 flex h-7 items-center gap-2 pe-1">
444
- <div className="min-w-0 flex-1 text-xs font-medium text-sidebar-foreground/70">
445
- {t("dispatch.sidebar.chats")}
446
- </div>
447
- <Tooltip>
448
- <TooltipTrigger asChild>
449
- <button
450
- type="button"
451
- onClick={handleNewChat}
452
- className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-sidebar-foreground/65 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
453
- aria-label={t("dispatch.sidebar.newDispatchChat")}
454
- >
455
- <IconPlus className="size-3.5" />
456
- </button>
457
- </TooltipTrigger>
458
- <TooltipContent>{t("dispatch.sidebar.newChat")}</TooltipContent>
459
- </Tooltip>
460
- </div>
461
- <div className="grid gap-0.5">
462
- {visibleThreads.length > 0 ? (
463
- visibleThreads.map((thread) => {
464
- const localPathname = localDispatchPath(location.pathname);
465
- const isActive =
466
- thread.id ===
467
- (threadIdFromPath(localPathname) ??
468
- (localPathname === "/chat" ? null : activeThreadId));
469
- const isRenaming = thread.id === renamingThreadId;
470
- return (
471
- <div
472
- key={thread.id}
473
- className={cn(
474
- "group flex h-8 min-w-0 items-center rounded-md text-sm transition-colors",
475
- isActive
476
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
477
- : "text-sidebar-foreground/80 hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground",
478
- )}
455
+ <div className="ms-4 min-w-0 space-y-0.5">
456
+ {chatsLoading &&
457
+ visibleThreads.length === 0 &&
458
+ Array.from({ length: 3 }).map((_, index) => (
459
+ <div
460
+ key={`chat-skeleton-${index}`}
461
+ className="flex items-center gap-2 px-3 py-1"
462
+ >
463
+ <Skeleton className="size-3.5 shrink-0 rounded-sm" />
464
+ <Skeleton className="h-3 w-3/4 rounded" />
465
+ </div>
466
+ ))}
467
+ {visibleThreads.map((thread) => {
468
+ const localPathname = localDispatchPath(location.pathname);
469
+ const isActive =
470
+ thread.id ===
471
+ (threadIdFromPath(localPathname) ??
472
+ (localPathname === "/chat" ? null : activeThreadId));
473
+ const isRenaming = thread.id === renamingThreadId;
474
+ const title = threadTitle(thread, t("dispatch.sidebar.newChat"));
475
+ return (
476
+ <div
477
+ key={thread.id}
478
+ className={cn(
479
+ "group/item relative flex min-w-0 items-center rounded-lg transition-colors",
480
+ isActive
481
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
482
+ : "text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground",
483
+ )}
484
+ >
485
+ {isRenaming ? (
486
+ <form
487
+ onSubmit={handleRenameSubmit}
488
+ className="flex min-w-0 flex-1 items-center px-1"
479
489
  >
480
- {isRenaming ? (
481
- <form
482
- onSubmit={handleRenameSubmit}
483
- className="flex h-full min-w-0 flex-1 items-center px-1.5"
484
- >
485
- <Input
486
- ref={renameInputRef}
487
- value={renameDraft}
488
- onChange={(event) => setRenameDraft(event.target.value)}
489
- onBlur={() => void commitRenameThread()}
490
- onKeyDown={(event) => {
491
- if (event.key === "Escape") {
492
- event.preventDefault();
493
- cancelRenameThread();
494
- }
495
- }}
496
- maxLength={160}
497
- aria-label={t("dispatch.sidebar.renameThread", {
498
- title: threadTitle(
499
- thread,
500
- t("dispatch.sidebar.newChat"),
501
- ),
502
- })}
503
- className="h-6 min-w-0 rounded-sm border-sidebar-border bg-background px-1.5 text-xs"
504
- />
505
- </form>
506
- ) : (
507
- <>
490
+ <Input
491
+ ref={renameInputRef}
492
+ value={renameDraft}
493
+ onChange={(event) => setRenameDraft(event.target.value)}
494
+ onBlur={() => void commitRenameThread()}
495
+ onKeyDown={(event) => {
496
+ if (event.key === "Escape") {
497
+ event.preventDefault();
498
+ cancelRenameThread();
499
+ }
500
+ }}
501
+ maxLength={160}
502
+ aria-label={t("dispatch.sidebar.renameThread", { title })}
503
+ className="h-6 min-w-0 rounded-sm border-sidebar-border bg-background px-1.5 text-xs"
504
+ />
505
+ </form>
506
+ ) : (
507
+ <>
508
+ <Tooltip>
509
+ <TooltipTrigger asChild>
508
510
  <button
509
511
  type="button"
510
512
  onClick={() => openThread(thread.id)}
511
- className="flex h-full min-w-0 flex-1 cursor-pointer items-center px-2 text-start outline-none focus-visible:ring-2 focus-visible:ring-ring"
513
+ className="min-w-0 flex-1 cursor-pointer px-2 py-1.5 pe-12 text-start text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
512
514
  >
513
- <span className="min-w-0 flex-1 truncate">
514
- {threadTitle(thread, t("dispatch.sidebar.newChat"))}
515
- </span>
515
+ <span className="block min-w-0 truncate">{title}</span>
516
516
  </button>
517
- <div className="relative flex size-7 shrink-0 items-center justify-end pe-1">
518
- <span className="text-[11px] text-sidebar-foreground/50 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0">
519
- {isActive
520
- ? ""
521
- : formatThreadAge(
522
- threadUpdatedAt(thread),
523
- formatDate,
524
- )}
525
- </span>
526
- <DropdownMenu>
517
+ </TooltipTrigger>
518
+ <TooltipContent side="right">{title}</TooltipContent>
519
+ </Tooltip>
520
+ <div className="pointer-events-none absolute end-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5">
521
+ <span className="pointer-events-none pe-1 text-[11px] text-muted-foreground/60 transition-opacity group-hover/item:opacity-0 group-focus-within/item:opacity-0">
522
+ {isActive
523
+ ? ""
524
+ : formatThreadAge(threadUpdatedAt(thread), formatDate)}
525
+ </span>
526
+ <DropdownMenu>
527
+ <Tooltip>
528
+ <TooltipTrigger asChild>
527
529
  <DropdownMenuTrigger asChild>
528
530
  <button
529
531
  type="button"
530
532
  aria-label={t("dispatch.sidebar.chatOptions", {
531
- title: threadTitle(
532
- thread,
533
- t("dispatch.sidebar.newChat"),
534
- ),
533
+ title,
535
534
  })}
536
- className="absolute end-1 flex size-6 cursor-pointer items-center justify-center rounded-md text-sidebar-foreground/65 opacity-0 transition-opacity hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100"
535
+ className="pointer-events-auto rounded p-0.5 text-muted-foreground/50 opacity-0 transition-all hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/item:opacity-100 group-focus-within/item:opacity-100 data-[state=open]:opacity-100 data-[state=open]:text-foreground"
537
536
  >
538
- <IconDots className="size-4" />
537
+ <IconDots className="size-3" />
539
538
  </button>
540
539
  </DropdownMenuTrigger>
541
- <DropdownMenuContent
542
- align="end"
543
- side="right"
544
- sideOffset={6}
545
- >
546
- <DropdownMenuItem
547
- onSelect={() => startRenameThread(thread)}
548
- >
549
- <IconEdit className="size-4" />
550
- {t("dispatch.sidebar.renameChat")}
551
- </DropdownMenuItem>
552
- </DropdownMenuContent>
553
- </DropdownMenu>
554
- </div>
555
- </>
556
- )}
557
- </div>
558
- );
559
- })
560
- ) : (
561
- <button
562
- type="button"
563
- onClick={handleNewChat}
564
- className="flex h-8 cursor-pointer items-center rounded-md px-2 text-start text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/65 hover:text-sidebar-accent-foreground"
565
- >
566
- <span className="truncate">{t("dispatch.sidebar.newChat")}</span>
567
- </button>
568
- )}
569
- </div>
540
+ </TooltipTrigger>
541
+ <TooltipContent side="right">
542
+ {t("dispatch.sidebar.chatOptions", { title })}
543
+ </TooltipContent>
544
+ </Tooltip>
545
+ <DropdownMenuContent
546
+ align="start"
547
+ side="right"
548
+ className="w-44"
549
+ >
550
+ <DropdownMenuItem
551
+ onSelect={() => startRenameThread(thread)}
552
+ >
553
+ <IconEdit className="size-3.5" />
554
+ {t("dispatch.sidebar.renameChat")}
555
+ </DropdownMenuItem>
556
+ </DropdownMenuContent>
557
+ </DropdownMenu>
558
+ </div>
559
+ </>
560
+ )}
561
+ </div>
562
+ );
563
+ })}
564
+ <button
565
+ type="button"
566
+ onClick={() => void handleNewChat()}
567
+ className="flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-muted-foreground/60 transition-colors hover:bg-sidebar-accent/50 hover:text-foreground"
568
+ >
569
+ <IconPlus className="size-3 shrink-0" />
570
+ <span className="truncate">{t("dispatch.sidebar.newChat")}</span>
571
+ </button>
570
572
  </div>
571
573
  );
572
574
  }
@@ -604,7 +606,7 @@ export function NavContent({
604
606
  ...navItemsForSection(extensionNavItems, "operations"),
605
607
  ];
606
608
  const localPathname = localDispatchPath(location.pathname);
607
- const operationsOpen = operationsNavItems.some((item) =>
609
+ const advancedOpen = ADVANCED_NAV_ITEMS.some((item) =>
608
610
  navItemMatchesPath(item, localPathname),
609
611
  );
610
612
  const navLabel = (item: DispatchNavItem) => {
@@ -720,36 +722,27 @@ export function NavContent({
720
722
  <div
721
723
  className={cn(
722
724
  "flex items-center",
723
- collapsed ? "justify-center" : "gap-3",
725
+ collapsed ? "justify-center" : "gap-2",
724
726
  )}
725
727
  >
726
728
  {!collapsed && (
727
729
  <>
728
- <div className="flex h-9 w-9 items-center justify-center rounded-xl border bg-card text-foreground">
729
- <img
730
- src={appPath("/agent-native-icon-light.svg")}
731
- alt=""
732
- aria-hidden="true"
733
- className="block h-4 w-auto shrink-0 dark:hidden"
734
- />
735
- <img
736
- src={appPath("/agent-native-icon-dark.svg")}
737
- alt=""
738
- aria-hidden="true"
739
- className="hidden h-4 w-auto shrink-0 dark:block"
740
- />
741
- </div>
730
+ <img
731
+ src={appPath("/agent-native-icon-light.svg")}
732
+ alt=""
733
+ aria-hidden="true"
734
+ className="block h-5 w-auto shrink-0 dark:hidden"
735
+ />
736
+ <img
737
+ src={appPath("/agent-native-icon-dark.svg")}
738
+ alt=""
739
+ aria-hidden="true"
740
+ className="hidden h-5 w-auto shrink-0 dark:block"
741
+ />
742
742
  <div className="min-w-0 flex-1">
743
- <div className="truncate text-sm font-semibold text-foreground">
743
+ <div className="truncate text-lg font-bold tracking-tight text-foreground">
744
744
  {workspaceLabel ?? "Dispatch"}
745
745
  </div>
746
- <div className="truncate text-xs text-muted-foreground">
747
- {workspaceLabel
748
- ? t("dispatch.sidebar.workspaceSubtitle", {
749
- count: ws?.appCount ?? 0,
750
- })
751
- : t("dispatch.sidebar.workspaceControlPlane")}
752
- </div>
753
746
  </div>
754
747
  </>
755
748
  )}
@@ -791,30 +784,44 @@ export function NavContent({
791
784
  collapsed && "flex flex-col items-center",
792
785
  )}
793
786
  >
794
- {(collapsed
795
- ? [...primaryNavItems, ...operationsNavItems]
796
- : primaryNavItems
797
- ).map(renderNavItem)}
787
+ {primaryNavItems.map(renderNavItem)}
798
788
  </ul>
799
- </nav>
800
789
 
801
- {!collapsed ? (
802
- <div className="mt-auto shrink-0">
803
- <div className="px-2 py-2">
804
- <details className="group" open={operationsOpen}>
805
- <summary className="flex h-8 cursor-pointer list-none items-center justify-between rounded-md px-2 text-xs font-medium uppercase text-sidebar-foreground/50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&::-webkit-details-marker]:hidden">
806
- <span>{t("dispatch.nav.operations")}</span>
790
+ {collapsed ? (
791
+ <ul className="mt-2 flex flex-col items-center space-y-0.5">
792
+ {[...operationsNavItems, ...ADVANCED_NAV_ITEMS].map(
793
+ renderNavItem,
794
+ )}
795
+ </ul>
796
+ ) : (
797
+ <div className="mt-5">
798
+ <p className="px-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-sidebar-foreground/45">
799
+ {t("dispatch.nav.operate", { defaultValue: "Operate" })}
800
+ </p>
801
+ <ul className="space-y-0.5">
802
+ {operationsNavItems.map(renderNavItem)}
803
+ </ul>
804
+
805
+ <details className="group mt-3" open={advancedOpen}>
806
+ <summary className="flex h-8 cursor-pointer list-none items-center justify-between rounded-md px-2 text-xs font-medium text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&::-webkit-details-marker]:hidden">
807
+ <span>
808
+ {t("dispatch.nav.advanced", { defaultValue: "Advanced" })}
809
+ </span>
807
810
  <IconChevronDown
808
811
  size={14}
809
812
  className="transition-transform group-open:rotate-180"
810
813
  />
811
814
  </summary>
812
815
  <ul className="mt-1 space-y-0.5">
813
- {operationsNavItems.map(renderNavItem)}
816
+ {ADVANCED_NAV_ITEMS.map(renderNavItem)}
814
817
  </ul>
815
818
  </details>
816
819
  </div>
820
+ )}
821
+ </nav>
817
822
 
823
+ {!collapsed ? (
824
+ <div className="mt-auto shrink-0">
818
825
  <div className="px-2 py-1">
819
826
  <ExtensionsSidebarSection />
820
827
  </div>