@gperezmz/bb-plugin-thread-glance 0.1.2

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/THIRD_PARTY_NOTICES.md +230 -0
  4. package/app.tsx +12 -0
  5. package/components/ui/alert-dialog.tsx +142 -0
  6. package/components/ui/button.tsx +59 -0
  7. package/components/ui/card.tsx +83 -0
  8. package/components/ui/coarse-pointer-sizing.ts +66 -0
  9. package/components/ui/context-menu.tsx +355 -0
  10. package/components/ui/dialog.tsx +416 -0
  11. package/components/ui/dropdown-menu.tsx +640 -0
  12. package/components/ui/hooks/use-compact-viewport.tsx +37 -0
  13. package/components/ui/hooks/use-media-query.ts +70 -0
  14. package/components/ui/hooks/use-pointer-coarse.ts +7 -0
  15. package/components/ui/hover-card.tsx +36 -0
  16. package/components/ui/icon-extended.tsx +333 -0
  17. package/components/ui/icon-registry.ts +189 -0
  18. package/components/ui/icon.tsx +444 -0
  19. package/components/ui/input.tsx +30 -0
  20. package/components/ui/menu-item-hover.tsx +93 -0
  21. package/components/ui/motion.ts +4 -0
  22. package/components/ui/overlay-trigger.ts +84 -0
  23. package/components/ui/popover.tsx +223 -0
  24. package/components/ui/responsive-overlay.tsx +823 -0
  25. package/components/ui/tooltip.tsx +63 -0
  26. package/dist/app.css +2 -0
  27. package/dist/app.js +66 -0
  28. package/dist/app.meta.json +11 -0
  29. package/dist/server.js +71 -0
  30. package/dist/server.js.map +6 -0
  31. package/dist/server.meta.json +11 -0
  32. package/features/thread-list/components/Dialogs.tsx +299 -0
  33. package/features/thread-list/components/FoldRows.tsx +147 -0
  34. package/features/thread-list/components/GroupSection.tsx +370 -0
  35. package/features/thread-list/components/ProviderBadge.tsx +57 -0
  36. package/features/thread-list/components/PullRequestBadge.tsx +50 -0
  37. package/features/thread-list/components/Rails.tsx +28 -0
  38. package/features/thread-list/components/RenameEditor.tsx +118 -0
  39. package/features/thread-list/components/RowMenu.tsx +168 -0
  40. package/features/thread-list/components/SplitMiniMap.tsx +55 -0
  41. package/features/thread-list/components/ThreadDetails.tsx +138 -0
  42. package/features/thread-list/components/ThreadList.tsx +855 -0
  43. package/features/thread-list/components/ThreadRowView.tsx +631 -0
  44. package/features/thread-list/components/Toolbar.tsx +296 -0
  45. package/features/thread-list/components/controller.ts +64 -0
  46. package/features/thread-list/components/glyphs.tsx +129 -0
  47. package/features/thread-list/components/row-card.ts +127 -0
  48. package/features/thread-list/data/storage.ts +18 -0
  49. package/features/thread-list/data/useAutoExpand.ts +53 -0
  50. package/features/thread-list/data/useClientPreferences.ts +20 -0
  51. package/features/thread-list/data/useNotes.ts +41 -0
  52. package/features/thread-list/data/useNow.ts +19 -0
  53. package/features/thread-list/data/usePreferences.ts +126 -0
  54. package/features/thread-list/data/useScheduled.ts +43 -0
  55. package/features/thread-list/data/useStamps.ts +68 -0
  56. package/features/thread-list/icons.ts +44 -0
  57. package/features/thread-list/index.ts +2 -0
  58. package/features/thread-list/model/attention.ts +89 -0
  59. package/features/thread-list/model/counters.ts +67 -0
  60. package/features/thread-list/model/details.ts +79 -0
  61. package/features/thread-list/model/drag.ts +104 -0
  62. package/features/thread-list/model/expansion.ts +71 -0
  63. package/features/thread-list/model/families.ts +297 -0
  64. package/features/thread-list/model/groups.ts +263 -0
  65. package/features/thread-list/model/labels.ts +50 -0
  66. package/features/thread-list/model/layout.ts +61 -0
  67. package/features/thread-list/model/menu.ts +78 -0
  68. package/features/thread-list/model/move.ts +46 -0
  69. package/features/thread-list/model/notes.ts +52 -0
  70. package/features/thread-list/model/provider-mark.ts +59 -0
  71. package/features/thread-list/model/share.ts +101 -0
  72. package/features/thread-list/model/sort.ts +78 -0
  73. package/features/thread-list/model/state.ts +330 -0
  74. package/features/thread-list/model/time.ts +107 -0
  75. package/features/thread-list/model/toggles.ts +99 -0
  76. package/features/thread-list/model/view.ts +669 -0
  77. package/features/thread-list/model/windowing.ts +27 -0
  78. package/features/thread-list/testing/fixtures.ts +162 -0
  79. package/hooks/useBrowserDimmingModal.ts +5 -0
  80. package/lib/portal-scope.ts +15 -0
  81. package/lib/utils.ts +17 -0
  82. package/package.json +96 -0
  83. package/server/cli.ts +107 -0
  84. package/server/failures.ts +136 -0
  85. package/server/import.ts +144 -0
  86. package/server/notes.ts +236 -0
  87. package/server/preference-store.ts +96 -0
  88. package/server/scheduled.ts +129 -0
  89. package/server/serial.ts +12 -0
  90. package/server/stamps.ts +142 -0
  91. package/server/startup.ts +121 -0
  92. package/server.ts +126 -0
  93. package/shared/contract.ts +73 -0
  94. package/shared/preferences.ts +241 -0
  95. package/shared/signals.ts +71 -0
  96. package/skills/thread-glance/SKILL.md +69 -0
  97. package/tsconfig.json +36 -0
  98. package/types/global-jsx.d.ts +10 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "sdkMajor": 0,
3
+ "sdkVersion": "0.5.9",
4
+ "artifactFormatVersion": 1,
5
+ "pluginId": "thread-glance",
6
+ "pluginVersion": "0.1.2",
7
+ "builtWith": {
8
+ "bbVersion": "0.43.4",
9
+ "pluginSdkVersion": "0.5.9"
10
+ }
11
+ }
@@ -0,0 +1,299 @@
1
+ // Dialogs the list opens: customize, new section, confirmations and the
2
+ // compact details drawer. Dialogs become drawers on compact.
3
+ import { useEffect, useState } from "react";
4
+ import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogTitle,
14
+ } from "@/components/ui/alert-dialog";
15
+ import { Button } from "@/components/ui/button";
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogFooter,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ } from "@/components/ui/dialog";
24
+ import { Input } from "@/components/ui/input";
25
+ import { cn } from "@/lib/utils";
26
+ import { ICONS } from "../icons";
27
+
28
+ export interface CustomizeItem {
29
+ id: string;
30
+ label: string;
31
+ hidden: boolean;
32
+ /** Pinned can't be hidden. */
33
+ hideable: boolean;
34
+ }
35
+
36
+ /** "Customize list": eye toggles and reorder. */
37
+ export function CustomizeDialog({
38
+ open,
39
+ items,
40
+ onOpenChange,
41
+ onToggleHidden,
42
+ onMove,
43
+ }: {
44
+ open: boolean;
45
+ items: readonly CustomizeItem[];
46
+ onOpenChange(open: boolean): void;
47
+ onToggleHidden(id: string): void;
48
+ onMove(id: string, direction: -1 | 1): void;
49
+ }) {
50
+ return (
51
+ <Dialog open={open} onOpenChange={onOpenChange}>
52
+ <DialogContent className="max-w-sm">
53
+ <DialogHeader>
54
+ <DialogTitle>Customize list</DialogTitle>
55
+ <DialogDescription>Hidden groups move into More. Their counters still show there.</DialogDescription>
56
+ </DialogHeader>
57
+ <ul className="flex flex-col gap-0.5" aria-label="Groups">
58
+ {items.map((item, index) => (
59
+ <li
60
+ key={item.id}
61
+ data-sidebar-customize-item={item.id}
62
+ className="flex items-center gap-1 rounded-md py-0.5 pl-2 pr-1 text-sm hover:bg-accent"
63
+ >
64
+ <span className={cn("min-w-0 flex-1 truncate", item.hidden && "text-muted-foreground")}>{item.label}</span>
65
+ <Button
66
+ variant="ghost"
67
+ size="icon"
68
+ className="size-7"
69
+ aria-label={`Move ${item.label} up`}
70
+ disabled={index === 0}
71
+ onClick={() => onMove(item.id, -1)}
72
+ >
73
+ <Icon name={ICONS.moveUp} aria-hidden />
74
+ </Button>
75
+ <Button
76
+ variant="ghost"
77
+ size="icon"
78
+ className="size-7"
79
+ aria-label={`Move ${item.label} down`}
80
+ disabled={index === items.length - 1}
81
+ onClick={() => onMove(item.id, 1)}
82
+ >
83
+ <Icon name={ICONS.collapse} aria-hidden />
84
+ </Button>
85
+ <Button
86
+ variant="ghost"
87
+ size="icon"
88
+ className="size-7"
89
+ aria-pressed={!item.hidden}
90
+ aria-label={item.hidden ? `Show ${item.label}` : `Hide ${item.label}`}
91
+ disabled={!item.hideable}
92
+ onClick={() => onToggleHidden(item.id)}
93
+ >
94
+ <Icon name={item.hidden ? ICONS.hidden : ICONS.show} aria-hidden />
95
+ </Button>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ <DialogFooter>
100
+ <Button onClick={() => onOpenChange(false)}>Done</Button>
101
+ </DialogFooter>
102
+ </DialogContent>
103
+ </Dialog>
104
+ );
105
+ }
106
+
107
+ export function NewSectionDialog({
108
+ open,
109
+ onOpenChange,
110
+ onCreate,
111
+ }: {
112
+ open: boolean;
113
+ onOpenChange(open: boolean): void;
114
+ onCreate(name: string): Promise<void>;
115
+ }) {
116
+ const [name, setName] = useState("");
117
+ const [busy, setBusy] = useState(false);
118
+ useEffect(() => {
119
+ if (open) setName("");
120
+ }, [open]);
121
+ return (
122
+ <Dialog open={open} onOpenChange={onOpenChange}>
123
+ <DialogContent className="max-w-sm">
124
+ <form
125
+ className="flex flex-col gap-4"
126
+ onSubmit={async (event) => {
127
+ event.preventDefault();
128
+ const trimmed = name.trim();
129
+ if (trimmed === "") return;
130
+ setBusy(true);
131
+ try {
132
+ await onCreate(trimmed);
133
+ onOpenChange(false);
134
+ } finally {
135
+ setBusy(false);
136
+ }
137
+ }}
138
+ >
139
+ <DialogHeader>
140
+ <DialogTitle>New section</DialogTitle>
141
+ </DialogHeader>
142
+ <Input autoFocus aria-label="Section name" value={name} onChange={(event) => setName(event.target.value)} />
143
+ <DialogFooter>
144
+ <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
145
+ Cancel
146
+ </Button>
147
+ <Button type="submit" disabled={busy || name.trim() === ""}>
148
+ Create
149
+ </Button>
150
+ </DialogFooter>
151
+ </form>
152
+ </DialogContent>
153
+ </Dialog>
154
+ );
155
+ }
156
+
157
+ export function ConfirmDialog({
158
+ open,
159
+ title,
160
+ description,
161
+ confirmLabel,
162
+ destructive,
163
+ onOpenChange,
164
+ onConfirm,
165
+ }: {
166
+ open: boolean;
167
+ title: string;
168
+ description: string;
169
+ confirmLabel: string;
170
+ destructive?: boolean;
171
+ onOpenChange(open: boolean): void;
172
+ onConfirm(): void;
173
+ }) {
174
+ return (
175
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
176
+ <AlertDialogContent>
177
+ <AlertDialogHeader>
178
+ <AlertDialogTitle>{title}</AlertDialogTitle>
179
+ <AlertDialogDescription>{description}</AlertDialogDescription>
180
+ </AlertDialogHeader>
181
+ <AlertDialogFooter>
182
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
183
+ <AlertDialogAction
184
+ className={cn(destructive && "bg-destructive text-destructive-foreground hover:bg-destructive/90")}
185
+ onClick={onConfirm}
186
+ >
187
+ {confirmLabel}
188
+ </AlertDialogAction>
189
+ </AlertDialogFooter>
190
+ </AlertDialogContent>
191
+ </AlertDialog>
192
+ );
193
+ }
194
+
195
+ export function DetailsDialog({
196
+ open,
197
+ title,
198
+ onOpenChange,
199
+ children,
200
+ }: {
201
+ open: boolean;
202
+ title: string;
203
+ onOpenChange(open: boolean): void;
204
+ children: React.ReactNode;
205
+ }) {
206
+ return (
207
+ <Dialog open={open} onOpenChange={onOpenChange}>
208
+ <DialogContent className="max-w-sm">
209
+ <DialogHeader>
210
+ <DialogTitle className="sr-only">{title}</DialogTitle>
211
+ </DialogHeader>
212
+ {children}
213
+ </DialogContent>
214
+ </Dialog>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * "Move…": nest a thread under another, or move it to the top
220
+ * level, from the keyboard. Arrow keys pick, Enter moves.
221
+ */
222
+ export function MoveDialog({
223
+ open,
224
+ title,
225
+ targets,
226
+ query,
227
+ onQueryChange,
228
+ onOpenChange,
229
+ onMove,
230
+ }: {
231
+ open: boolean;
232
+ title: string;
233
+ targets: readonly { parentThreadId: string | null; label: string; detail: string | null }[];
234
+ query: string;
235
+ onQueryChange(query: string): void;
236
+ onOpenChange(open: boolean): void;
237
+ onMove(parentThreadId: string | null): void;
238
+ }) {
239
+ const [index, setIndex] = useState(0);
240
+ useEffect(() => setIndex(0), [query, open]);
241
+ const choose = (target: (typeof targets)[number] | undefined) => {
242
+ if (target === undefined) return;
243
+ onMove(target.parentThreadId);
244
+ onOpenChange(false);
245
+ };
246
+ return (
247
+ <Dialog open={open} onOpenChange={onOpenChange}>
248
+ <DialogContent className="max-w-sm">
249
+ <DialogHeader>
250
+ <DialogTitle>Move “{title}”</DialogTitle>
251
+ <DialogDescription>Nest it under another thread, or move it to the top level.</DialogDescription>
252
+ </DialogHeader>
253
+ <Input
254
+ autoFocus
255
+ role="combobox"
256
+ aria-expanded
257
+ aria-controls="thread-glance-move-targets"
258
+ aria-activedescendant={targets[index] ? `thread-glance-move-${index}` : undefined}
259
+ aria-label="Find a thread"
260
+ placeholder="Find a thread"
261
+ value={query}
262
+ onChange={(event) => onQueryChange(event.target.value)}
263
+ onKeyDown={(event) => {
264
+ if (event.key === "ArrowDown") {
265
+ event.preventDefault();
266
+ setIndex((value) => Math.min(value + 1, targets.length - 1));
267
+ } else if (event.key === "ArrowUp") {
268
+ event.preventDefault();
269
+ setIndex((value) => Math.max(value - 1, 0));
270
+ } else if (event.key === "Enter") {
271
+ event.preventDefault();
272
+ choose(targets[index]);
273
+ }
274
+ }}
275
+ />
276
+ <ul id="thread-glance-move-targets" role="listbox" aria-label="Move to" className="flex max-h-72 flex-col gap-0.5 overflow-y-auto">
277
+ {targets.length === 0 ? <li className="px-2 py-1.5 text-sm text-muted-foreground">No matching threads</li> : null}
278
+ {targets.map((target, position) => (
279
+ <li
280
+ key={target.parentThreadId ?? "top"}
281
+ id={`thread-glance-move-${position}`}
282
+ role="option"
283
+ aria-selected={position === index}
284
+ onMouseEnter={() => setIndex(position)}
285
+ onClick={() => choose(target)}
286
+ className={cn(
287
+ "flex cursor-pointer flex-col rounded-md px-2 py-1.5 text-sm",
288
+ position === index && "bg-accent",
289
+ )}
290
+ >
291
+ <span className="truncate">{target.label}</span>
292
+ {target.detail ? <span className="truncate text-xs text-muted-foreground">{target.detail}</span> : null}
293
+ </li>
294
+ ))}
295
+ </ul>
296
+ </DialogContent>
297
+ </Dialog>
298
+ );
299
+ }
@@ -0,0 +1,147 @@
1
+ // The fold rows (`N older` on a group, `N more child threads` on a family)
2
+ // and the environment folder row.
3
+ import { memo, useState } from "react";
4
+ import { experimental_Icon as Icon, experimental_ProviderIcon as ProviderIcon } from "@get-bb/plugin-sdk/app";
5
+ import type { PluginEnvironmentProvider } from "@get-bb/plugin-sdk/app";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from "@/components/ui/dropdown-menu";
12
+ import { cn } from "@/lib/utils";
13
+ import { ICONS } from "../icons";
14
+ import { RowRails } from "./Rails";
15
+ import { olderRowText } from "../model/labels";
16
+ import { rowIndent } from "../model/layout";
17
+ import { EMPTY_LEVEL_TEXT, type EmptyRow, type EnvironmentRow, type OlderRow } from "../model/view";
18
+ import type { RowController } from "./controller";
19
+ import { FlagGlyph } from "./glyphs";
20
+ import { RenameEditor } from "./RenameEditor";
21
+ import { ROW_ICON_BUTTON } from "./ThreadRowView";
22
+
23
+ export const OlderRowView = memo(function OlderRowView({ row, controller }: { row: OlderRow; controller: RowController }) {
24
+ const { label, ariaLabel } = olderRowText(row);
25
+ // A family's fold sits where its children do, with a dots glyph in the
26
+ // status slot; the group's fold keeps the chevron.
27
+ const inFamily = row.scope !== "group";
28
+ return (
29
+ <button
30
+ type="button"
31
+ aria-expanded={row.expanded}
32
+ aria-label={ariaLabel}
33
+ onClick={() => controller.onToggleOlder(row)}
34
+ className={cn(
35
+ "relative flex w-full items-center rounded-md pr-2 text-left text-xs text-muted-foreground outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
36
+ inFamily ? "h-7 gap-1.5 max-md:pointer-coarse:h-9" : "h-6 gap-1",
37
+ )}
38
+ style={{ paddingLeft: rowIndent(row.depth, controller.nesting) + (inFamily ? 0 : 2) }}
39
+ >
40
+ <RowRails rails={row.rails} nesting={controller.nesting} />
41
+ {inFamily ? (
42
+ <span aria-hidden className="inline-flex size-4 shrink-0 items-center justify-center">
43
+ <Icon name={ICONS.more} className="size-3.5" />
44
+ </span>
45
+ ) : (
46
+ <Icon name={ICONS.expand} aria-hidden className={cn("size-3 transition-transform", row.expanded && "-rotate-90")} />
47
+ )}
48
+ {label}
49
+ </button>
50
+ );
51
+ });
52
+
53
+ /** A plain line under an opened chip that Needs attention leaves empty; not a control. */
54
+ export const EmptyRowView = memo(function EmptyRowView({ row, controller }: { row: EmptyRow; controller: RowController }) {
55
+ return (
56
+ <div
57
+ className="relative flex h-7 w-full items-center gap-1.5 pr-2 text-xs text-muted-foreground max-md:pointer-coarse:h-9"
58
+ style={{ paddingLeft: rowIndent(row.depth, controller.nesting) }}
59
+ >
60
+ <RowRails rails={row.rails} nesting={controller.nesting} />
61
+ <span aria-hidden className="size-4 shrink-0" />
62
+ <span>{EMPTY_LEVEL_TEXT}</span>
63
+ </div>
64
+ );
65
+ });
66
+
67
+ export const EnvironmentRowView = memo(function EnvironmentRowView({
68
+ row,
69
+ controller,
70
+ environmentProviders,
71
+ }: {
72
+ row: EnvironmentRow;
73
+ controller: RowController;
74
+ environmentProviders: readonly PluginEnvironmentProvider[];
75
+ }) {
76
+ const [renaming, setRenaming] = useState(false);
77
+ const provider =
78
+ row.environmentProviderId === null
79
+ ? null
80
+ : environmentProviders.find((candidate) => candidate.id === row.environmentProviderId) ?? null;
81
+ return (
82
+ <div
83
+ className="group/row relative flex h-7 w-full items-center gap-1.5 rounded-md pr-1 text-sm text-muted-foreground hover:bg-sidebar-accent"
84
+ style={{ paddingLeft: rowIndent(row.depth, controller.nesting) }}
85
+ >
86
+ <RowRails rails={row.rails} nesting={controller.nesting} />
87
+ <button
88
+ type="button"
89
+ aria-expanded={!row.collapsed}
90
+ aria-label={`${row.collapsed ? "Expand" : "Collapse"} ${row.label} environment, ${row.threadIds.length} threads`}
91
+ onClick={() => controller.onToggleEnvironment(row.environmentId)}
92
+ className="absolute inset-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
93
+ />
94
+ <span className="pointer-events-none relative inline-flex size-4 shrink-0 items-center justify-center">
95
+ {provider !== null ? (
96
+ <ProviderIcon providerKind="environment" provider={provider} className="size-3.5" />
97
+ ) : (
98
+ <Icon name={row.environmentProviderId === null ? ICONS.machine : ICONS.environmentFallback} aria-hidden className="size-3.5" />
99
+ )}
100
+ </span>
101
+ {renaming ? (
102
+ <RenameEditor
103
+ initial={row.label}
104
+ label="Environment name"
105
+ onSave={(name) => controller.onRenameEnvironment(row.environmentId, name)}
106
+ onDone={() => setRenaming(false)}
107
+ />
108
+ ) : (
109
+ <span className="pointer-events-none relative min-w-0 flex-1 truncate">{row.label}</span>
110
+ )}
111
+ {row.flag !== null ? <FlagGlyph flag={row.flag} className="pointer-events-none relative size-3.5" /> : null}
112
+ <span className="pointer-events-none relative text-[11px] tabular-nums">{row.threadIds.length}</span>
113
+ <Icon
114
+ name={ICONS.expand}
115
+ aria-hidden
116
+ className={cn("pointer-events-none relative size-3 transition-transform", !row.collapsed && "rotate-90")}
117
+ />
118
+ <DropdownMenu>
119
+ <DropdownMenuTrigger asChild>
120
+ <button
121
+ type="button"
122
+ aria-label="Environment actions"
123
+ className={cn(ROW_ICON_BUTTON, !controller.compact && "opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100")}
124
+ >
125
+ <Icon name={ICONS.more} aria-hidden className="size-4" />
126
+ </button>
127
+ </DropdownMenuTrigger>
128
+ <DropdownMenuContent align="end">
129
+ <DropdownMenuItem
130
+ onSelect={() => controller.onNewThreadInEnvironment(row.environmentId, row.projectId, row.sectionId)}
131
+ >
132
+ <Icon name={ICONS.newThread} aria-hidden className="size-4" />
133
+ New thread in environment
134
+ </DropdownMenuItem>
135
+ <DropdownMenuItem onSelect={() => setRenaming(true)}>
136
+ <Icon name={ICONS.rename} aria-hidden className="size-4" />
137
+ Rename
138
+ </DropdownMenuItem>
139
+ <DropdownMenuItem onSelect={() => controller.onArchiveEnvironment(row.environmentId)}>
140
+ <Icon name={ICONS.archive} aria-hidden className="size-4" />
141
+ Archive
142
+ </DropdownMenuItem>
143
+ </DropdownMenuContent>
144
+ </DropdownMenu>
145
+ </div>
146
+ );
147
+ });