aidevops 3.31.80 → 3.32.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.
package/README.md CHANGED
@@ -60,7 +60,7 @@ The result: an AI operations platform that manages projects across every busines
60
60
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
61
61
 
62
62
  <!-- Release & Version Info -->
63
- [![Version](https://img.shields.io/badge/Version-3.31.80-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
63
+ [![Version](https://img.shields.io/badge/Version-3.32.0-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
64
64
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
65
65
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
66
66
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.31.80
1
+ 3.32.0
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.31.80
8
+ # Version: 3.32.0
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.31.80",
3
+ "version": "3.32.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -389,12 +389,56 @@ export const statusFixture: GuiStatusData = {
389
389
  label: "Fleet, sync, and secure messages",
390
390
  data_class: "mail/messages",
391
391
  labels: ["client-confidential"],
392
- surface_ids: ["devices", "messagingAccounts", "emailAccounts", "inbox", "campaigns", "cases", "projectConfig", "knowledge"],
392
+ surface_ids: ["devices", "messagingAccounts", "emailAccounts"],
393
393
  encrypted: true,
394
394
  state: "planned",
395
395
  preview_policy: "hidden_while_locked",
396
396
  actions_policy: "disabled_while_locked",
397
397
  },
398
+ {
399
+ id: "ai-session-history",
400
+ label: "AI sessions, worker threads, and prompts",
401
+ data_class: "session/history",
402
+ labels: ["confidential", "local-ai", "provider-policy"],
403
+ surface_ids: ["aiSessions", "workers"],
404
+ encrypted: true,
405
+ state: "planned",
406
+ preview_policy: "metadata_only",
407
+ actions_policy: "disabled_while_locked",
408
+ },
409
+ {
410
+ id: "repo-project-metadata",
411
+ label: "Repos, deployments, routines, and project metadata",
412
+ data_class: "repo/project metadata",
413
+ labels: ["internal", "client-confidential"],
414
+ surface_ids: ["git", "repos", "projects", "deployments", "routines"],
415
+ encrypted: true,
416
+ state: "planned",
417
+ preview_policy: "metadata_only",
418
+ actions_policy: "disabled_while_locked",
419
+ },
420
+ {
421
+ id: "infrastructure-inventory",
422
+ label: "Infrastructure, accounts, domains, and servers",
423
+ data_class: "infrastructure inventory",
424
+ labels: ["confidential", "credential-adjacent"],
425
+ surface_ids: ["vpnsProxies", "apps", "installation", "registrars", "hosts", "servers", "domains"],
426
+ encrypted: true,
427
+ state: "planned",
428
+ preview_policy: "metadata_only",
429
+ actions_policy: "disabled_while_locked",
430
+ },
431
+ {
432
+ id: "creator-business-records",
433
+ label: "Creator, business, and client records",
434
+ data_class: "business records",
435
+ labels: ["client-confidential", "personal-data"],
436
+ surface_ids: ["brands", "personas", "websites", "forums", "socialMedia", "marketplaces", "tasks", "contacts", "events", "notes", "bookmarks", "inbox", "campaigns", "cases", "projectConfig", "feedback", "knowledge", "maintenance", "performance", "reports"],
437
+ encrypted: true,
438
+ state: "planned",
439
+ preview_policy: "metadata_only",
440
+ actions_policy: "disabled_while_locked",
441
+ },
398
442
  ],
399
443
  devices: [
400
444
  {
@@ -8,6 +8,8 @@ import { DesktopStatusBar } from "./DesktopStatusBar";
8
8
  import { type NavigationHistory, nextHistoryIndex, useNavigationHistoryKeyboard } from "./navigation-history";
9
9
  import { ScreenshotCaptureNotificationHost } from "./ScreenshotCaptureNotification";
10
10
  import { fetchStatus, mockedStatus } from "./status-client";
11
+ import type { VaultDialogIntent } from "./VaultBadges";
12
+ import { VaultPassphraseModal } from "./VaultPassphraseModal";
11
13
 
12
14
  interface WebKitBridgeWindow extends Window {
13
15
  webkit?: {
@@ -105,6 +107,7 @@ export function App(): ReactElement {
105
107
  const [conversationMode, setConversationMode] = useState<ConversationMode>("ai");
106
108
  const [selectedLocalRepoIndex, setSelectedLocalRepoIndex] = useState(0);
107
109
  const [selectedSessionId, setSelectedSessionId] = useState<string | undefined>();
110
+ const [vaultDialogIntent, setVaultDialogIntent] = useState<VaultDialogIntent | null>(null);
108
111
  const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
109
112
  const resolvedTheme = themePreference === "system" ? systemTheme : themePreference;
110
113
  const activeSurface: SurfaceId = navigation.entries[navigation.index] ?? "overview";
@@ -209,6 +212,12 @@ export function App(): ReactElement {
209
212
  setSelectedLocalRepoIndex((current) => Math.min(current, Math.max(0, status.data.local_repos.repos.length - 1)));
210
213
  }, [status.data.local_repos.repos.length]);
211
214
 
215
+ useEffect(() => {
216
+ if (!statusLoading && status.data.vault.readiness.setup_required) {
217
+ setVaultDialogIntent("setup");
218
+ }
219
+ }, [status.data.vault.readiness.setup_required, statusLoading]);
220
+
212
221
  return (
213
222
  <main className={machineRailVisible ? "app-shell" : "app-shell machine-rail-collapsed"} aria-busy={statusLoading}>
214
223
  <div className="desktop-titlebar-tagline" aria-hidden="true">Your data protected. Your systems managed. Your creations published.</div>
@@ -221,6 +230,7 @@ export function App(): ReactElement {
221
230
  conversationMode={conversationMode}
222
231
  fontSizePreference={fontSizePreference}
223
232
  fontPreference={fontPreference}
233
+ onVaultRequest={(intent) => setVaultDialogIntent(intent)}
224
234
  selectedLocalRepoIndex={selectedLocalRepoIndex}
225
235
  selectedSessionId={selectedSessionId}
226
236
  setAccentHue={setAccentHue}
@@ -252,6 +262,7 @@ export function App(): ReactElement {
252
262
  fileRoot={fileRoot}
253
263
  goBack={goBack}
254
264
  goForward={goForward}
265
+ onVaultRequest={(intent) => setVaultDialogIntent(intent)}
255
266
  selectedLocalRepoIndex={selectedLocalRepoIndex}
256
267
  selectedSessionId={selectedSessionId}
257
268
  setActiveSurface={setActiveSurface}
@@ -263,6 +274,7 @@ export function App(): ReactElement {
263
274
  status={status.data}
264
275
  />
265
276
  <DesktopStatusBar status={status.data} />
277
+ {vaultDialogIntent ? <VaultPassphraseModal intent={vaultDialogIntent} onClose={() => setVaultDialogIntent(null)} vault={status.data.vault} /> : null}
266
278
  </main>
267
279
  );
268
280
  }
@@ -37,7 +37,7 @@ import {
37
37
  import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
38
38
  import { dashboardNavItem, navGroups, sidebarModeForSurface, surfaceRecordCounts, text } from "./app-model";
39
39
  import { SidebarFooter, wrappedOptionIndex } from "./AppearanceControls";
40
- import { VaultPadlock, vaultCollectionForSurface, vaultCollectionTooltip } from "./VaultBadges";
40
+ import { type VaultDialogIntent, VaultPadlock, vaultCollectionForSurface, vaultCollectionTooltip, vaultDialogIntentForStatus } from "./VaultBadges";
41
41
 
42
42
  function AidevopsPromptIcon() {
43
43
  return (
@@ -109,13 +109,14 @@ export function MachineRail({ machine }: { machine?: GuiMachineSummary }) {
109
109
  );
110
110
  }
111
111
 
112
- export function Sidebar({ activeSurface, accentHue, contrastPreference, conversationMode, fontPreference, fontSizePreference, selectedLocalRepoIndex, selectedSessionId, setAccentHue, setActiveSurface, setContrastPreference, setConversationMode, setFontPreference, setFontSizePreference, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, setShowBorders, setShowNavCounts, setThemePreference, shellMode, showBorders, showNavCounts, status, themePreference }: {
112
+ export function Sidebar({ activeSurface, accentHue, contrastPreference, conversationMode, fontPreference, fontSizePreference, onVaultRequest, selectedLocalRepoIndex, selectedSessionId, setAccentHue, setActiveSurface, setContrastPreference, setConversationMode, setFontPreference, setFontSizePreference, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, setShowBorders, setShowNavCounts, setThemePreference, shellMode, showBorders, showNavCounts, status, themePreference }: {
113
113
  activeSurface: SurfaceId;
114
114
  accentHue: number;
115
115
  contrastPreference: ContrastPreference;
116
116
  conversationMode: ConversationMode;
117
117
  fontPreference: FontPreference;
118
118
  fontSizePreference: FontSizePreference;
119
+ onVaultRequest: (intent: VaultDialogIntent) => void;
119
120
  selectedLocalRepoIndex: number;
120
121
  selectedSessionId: string | undefined;
121
122
  setAccentHue: (hue: number) => void;
@@ -151,9 +152,9 @@ export function Sidebar({ activeSurface, accentHue, contrastPreference, conversa
151
152
  {shellMode === "devices" ? <>
152
153
  <SidebarModeTabs mode={sidebarMode} setMode={(mode) => setSidebarMode(mode as SidebarMode)} />
153
154
  <ul className="sidebar-top-link">
154
- <SidebarItem activeSurface={activeSurface} item={dashboardNavItem} setActiveSurface={setActiveSurface} showCount={false} status={status} />
155
+ <SidebarItem activeSurface={activeSurface} item={dashboardNavItem} onVaultRequest={onVaultRequest} setActiveSurface={setActiveSurface} showCount={false} status={status} />
155
156
  </ul>
156
- {visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} status={status} />)}
157
+ {visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} onVaultRequest={onVaultRequest} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} status={status} />)}
157
158
  </> : <ConversationSidebar
158
159
  conversationMode={conversationMode}
159
160
  selectedLocalRepoIndex={selectedLocalRepoIndex}
@@ -310,9 +311,10 @@ function SidebarModeTabs<TMode extends SidebarMode | ConversationMode>({ mode, s
310
311
  );
311
312
  }
312
313
 
313
- function SidebarGroup({ activeSurface, group, recordCounts, setActiveSurface, showNavCounts, status }: {
314
+ function SidebarGroup({ activeSurface, group, onVaultRequest, recordCounts, setActiveSurface, showNavCounts, status }: {
314
315
  activeSurface: SurfaceId;
315
316
  group: SurfaceNavGroup;
317
+ onVaultRequest: (intent: VaultDialogIntent) => void;
316
318
  recordCounts: Partial<Record<SurfaceId, number>>;
317
319
  setActiveSurface: (surface: SurfaceId) => void;
318
320
  showNavCounts: boolean;
@@ -322,15 +324,16 @@ function SidebarGroup({ activeSurface, group, recordCounts, setActiveSurface, sh
322
324
  <section className="sidebar-group">
323
325
  <h2>{group.label}</h2>
324
326
  <ul>
325
- {group.items.map((item) => <SidebarItem activeSurface={activeSurface} item={item} key={item.id} recordCount={recordCounts[item.id]} setActiveSurface={setActiveSurface} showCount={showNavCounts} status={status} />)}
327
+ {group.items.map((item) => <SidebarItem activeSurface={activeSurface} item={item} key={item.id} onVaultRequest={onVaultRequest} recordCount={recordCounts[item.id]} setActiveSurface={setActiveSurface} showCount={showNavCounts} status={status} />)}
326
328
  </ul>
327
329
  </section>
328
330
  );
329
331
  }
330
332
 
331
- function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showCount, status }: {
333
+ function SidebarItem({ activeSurface, item, onVaultRequest, recordCount, setActiveSurface, showCount, status }: {
332
334
  activeSurface: SurfaceId;
333
335
  item: SurfaceNavItem;
336
+ onVaultRequest: (intent: VaultDialogIntent) => void;
334
337
  recordCount?: number;
335
338
  setActiveSurface: (surface: SurfaceId) => void;
336
339
  showCount: boolean;
@@ -348,7 +351,14 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
348
351
  aria-label={tooltip}
349
352
  aria-current={isActive ? "page" : undefined}
350
353
  className={isActive ? "surface-link active" : "surface-link"}
351
- onClick={() => setActiveSurface(item.id)}
354
+ onClick={(event) => {
355
+ if (vaultCollection && event.target instanceof HTMLElement && event.target.closest(".vault-padlock")) {
356
+ onVaultRequest(vaultDialogIntentForStatus(status.vault));
357
+ return;
358
+ }
359
+
360
+ setActiveSurface(item.id);
361
+ }}
352
362
  title={tooltip}
353
363
  type="button"
354
364
  >
@@ -1,6 +1,6 @@
1
1
  /* jshint esversion: 11 */
2
2
  import { type ReactElement, type ReactNode, useEffect, useState } from "react";
3
- import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiFileText, FiGlobe, FiHash, FiInfo, FiLogOut, FiMap, FiMessageSquare, FiPaperclip, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUser } from "react-icons/fi";
3
+ import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiFileText, FiGlobe, FiHash, FiInfo, FiLock, FiLogOut, FiMap, FiMessageSquare, FiPaperclip, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUnlock, FiUser } from "react-icons/fi";
4
4
  import type { GuiFileRootId, GuiNotificationSummary, GuiStatusData } from "../../gui-shared/src";
5
5
  import { SurfaceGlyph } from "./AppNavigation";
6
6
  import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
@@ -12,7 +12,7 @@ import { TamboConversationPart } from "./GenUiCards";
12
12
  import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
13
13
  import { PulseWorkersSurface } from "./PulseWorkersSurface";
14
14
  import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
15
- import { isVaultSurfaceLocked, vaultCollectionForSurface } from "./VaultBadges";
15
+ import { type VaultDialogIntent, isVaultSurfaceLocked, vaultCollectionForSurface, vaultDialogIntentForStatus } from "./VaultBadges";
16
16
  import { applyCommandPaletteSelection, useHeaderMenuState } from "./workspace-header-state";
17
17
  import { useWorkspaceTour, WorkspaceTourProvider } from "./WorkspaceTour";
18
18
 
@@ -21,7 +21,7 @@ const communityLinks = {
21
21
  x: "https://x.com/marcuswquinn",
22
22
  } as const;
23
23
 
24
- export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, selectedSessionId, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, shellMode, status }: {
24
+ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, onVaultRequest, selectedLocalRepoIndex, selectedSessionId, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, shellMode, status }: {
25
25
  activeItem: SurfaceNavItem;
26
26
  activeSectionLabel: string;
27
27
  activeSurface: SurfaceId;
@@ -31,6 +31,7 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
31
31
  fileRoot: GuiFileRootId | undefined;
32
32
  goBack: () => void;
33
33
  goForward: () => void;
34
+ onVaultRequest?: (intent: VaultDialogIntent) => void;
34
35
  selectedLocalRepoIndex: number;
35
36
  selectedSessionId: string | undefined;
36
37
  setActiveSurface: (surface: SurfaceId) => void;
@@ -41,21 +42,23 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
41
42
  shellMode: ShellMode;
42
43
  status: GuiStatusData;
43
44
  }) {
45
+ const requestVault = onVaultRequest ?? (() => undefined);
46
+
44
47
  return (
45
48
  <section className="app-inset" aria-label={text.workspaceLabel}>
46
49
  <WorkspaceTourProvider activeSurface={activeSurface}>
47
- <WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} setConversationMode={setConversationMode} setSelectedLocalRepoIndex={setSelectedLocalRepoIndex} setSelectedSessionId={setSelectedSessionId} setShellMode={setShellMode} status={status} />
50
+ <WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} onVaultRequest={requestVault} setActiveSurface={setActiveSurface} setConversationMode={setConversationMode} setSelectedLocalRepoIndex={setSelectedLocalRepoIndex} setSelectedSessionId={setSelectedSessionId} setShellMode={setShellMode} status={status} />
48
51
  <div className="workspace-scroll">
49
52
  {shellMode === "sessions"
50
53
  ? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSessionId} status={status} />
51
- : <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} openSurface={setActiveSurface} status={status} />}
54
+ : <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} onVaultRequest={requestVault} openSurface={setActiveSurface} status={status} />}
52
55
  </div>
53
56
  </WorkspaceTourProvider>
54
57
  </section>
55
58
  );
56
59
  }
57
60
 
58
- function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, status }: {
61
+ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, onVaultRequest, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, status }: {
59
62
  activeItem: SurfaceNavItem;
60
63
  activeSectionLabel: string;
61
64
  activeSurface: SurfaceId;
@@ -63,6 +66,7 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
63
66
  canGoForward: boolean;
64
67
  goBack: () => void;
65
68
  goForward: () => void;
69
+ onVaultRequest: (intent: VaultDialogIntent) => void;
66
70
  setActiveSurface: (surface: SurfaceId) => void;
67
71
  setConversationMode: (mode: ConversationMode) => void;
68
72
  setSelectedLocalRepoIndex: (index: number) => void;
@@ -146,7 +150,7 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
146
150
  <button aria-expanded={headerMenus.profileOpen} aria-label={`Open profile menu for ${userName}`} className="profile-avatar-button" onClick={headerMenus.toggleProfile} title={userName} type="button">
147
151
  <span>{userInitials}</span>
148
152
  </button>
149
- {headerMenus.profileOpen ? <ProfileMenu openSurface={(surface) => openItem({ surface })} userName={userName} /> : null}
153
+ {headerMenus.profileOpen ? <ProfileMenu onVaultRequest={onVaultRequest} openSurface={(surface) => openItem({ surface })} status={status} userName={userName} /> : null}
150
154
  </div>
151
155
  </div>
152
156
  {headerMenus.assistantOpen ? <AssistantPanel activeSurface={activeSurface} userName={userName} /> : null}
@@ -364,7 +368,9 @@ function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistan
364
368
  );
365
369
  }
366
370
 
367
- function ProfileMenu({ openSurface, userName }: { openSurface: (surface: SurfaceId) => void; userName: string }): ReactElement {
371
+ function ProfileMenu({ onVaultRequest, openSurface, status, userName }: { onVaultRequest: (intent: VaultDialogIntent) => void; openSurface: (surface: SurfaceId) => void; status: GuiStatusData; userName: string }): ReactElement {
372
+ const vaultIntent = vaultDialogIntentForStatus(status.vault);
373
+ const VaultIcon = vaultIntent === "lock" ? FiLock : FiUnlock;
368
374
  return (
369
375
  <div className="popover-menu profile-menu" role="menu">
370
376
  <div className="profile-menu-heading">
@@ -374,6 +380,7 @@ function ProfileMenu({ openSurface, userName }: { openSurface: (surface: Surface
374
380
  <button onClick={() => openSurface("settings")} role="menuitem" type="button"><FiSettings aria-hidden="true" /> Settings</button>
375
381
  <button onClick={() => openSurface("settings")} role="menuitem" type="button"><FiCommand aria-hidden="true" /> Theme</button>
376
382
  <button onClick={() => openSurface("settings")} role="menuitem" type="button"><FiGlobe aria-hidden="true" /> Language</button>
383
+ <button onClick={() => onVaultRequest(vaultIntent)} role="menuitem" type="button"><VaultIcon aria-hidden="true" /> {vaultIntent === "lock" ? "Lock Vault" : "Unlock Vault"}</button>
377
384
  <div className="menu-separator" />
378
385
  <strong>Community</strong>
379
386
  <a href={communityLinks.github} rel="noreferrer" role="menuitem" target="_blank"><FiMessageSquare aria-hidden="true" /> GitHub</a>
@@ -397,10 +404,11 @@ function AssistantPanel({ activeSurface, userName }: { activeSurface: SurfaceId;
397
404
  );
398
405
  }
399
406
 
400
- function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, status }: {
407
+ function SurfaceContent({ activeItem, activeSurface, fileRoot, onVaultRequest, openSurface, status }: {
401
408
  activeItem: SurfaceNavItem;
402
409
  activeSurface: SurfaceId;
403
410
  fileRoot: GuiFileRootId | undefined;
411
+ onVaultRequest: (intent: VaultDialogIntent) => void;
404
412
  openSurface: (surface: SurfaceId) => void;
405
413
  status: GuiStatusData;
406
414
  }) {
@@ -412,7 +420,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
412
420
  settings: <SettingsSurface status={status} />,
413
421
  notifications: <NotificationsSurface openSurface={openSurface} status={status} />,
414
422
  admin: <AdminSurface />,
415
- vault: <VaultSurface status={status} />,
423
+ vault: <VaultSurface onVaultRequest={onVaultRequest} status={status} />,
416
424
  aiSessions: <AiSessionsSurface selectedRepoIndex={0} status={status} />,
417
425
  channels: <CommsConversationSurface mode="channels" />,
418
426
  directMessages: <CommsConversationSurface mode="directMessages" />,
@@ -450,7 +458,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
450
458
  };
451
459
 
452
460
  if (isVaultSurfaceLocked(status.vault, activeSurface) && vaultCollection) {
453
- return <LockedVaultGate collection={vaultCollection} label={activeItem.label} vault={status.vault} />;
461
+ return <LockedVaultGate collection={vaultCollection} label={activeItem.label} onVaultRequest={onVaultRequest} vault={status.vault} />;
454
462
  }
455
463
 
456
464
  if (activeSurface === "git") {
@@ -4,7 +4,7 @@ import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiSetupTargetSummary,
4
4
  import { plannedHomes, text } from "./app-model";
5
5
  import { FileExplorerSurface } from "./FileExplorerSurface";
6
6
  import { PathActions } from "./PathActions";
7
- import { VaultPadlock } from "./VaultBadges";
7
+ import { type VaultDialogIntent, VaultPadlock, vaultDialogIntentForStatus } from "./VaultBadges";
8
8
 
9
9
  export { AiProvidersSurface } from "./AiProvidersSurface";
10
10
 
@@ -121,7 +121,7 @@ export function SecuritySurface({ status }: { status: GuiStatusData }) {
121
121
  );
122
122
  }
123
123
 
124
- export function VaultSurface({ status }: { status: GuiStatusData }) {
124
+ export function VaultSurface({ onVaultRequest, status }: { onVaultRequest: (intent: VaultDialogIntent) => void; status: GuiStatusData }) {
125
125
  const vault = status.vault;
126
126
  const vaultCollection = vault.collections.find((collection) => collection.surface_ids.includes("vault")) ?? vault.collections[0];
127
127
  const readiness = [
@@ -150,7 +150,7 @@ export function VaultSurface({ status }: { status: GuiStatusData }) {
150
150
  <h2>{text.vault}</h2>
151
151
  <p>{text.vaultIntro}</p>
152
152
  </div>
153
- {vaultCollection ? <VaultPadlock collection={vaultCollection} vault={vault} /> : null}
153
+ {vaultCollection ? <VaultPadlock collection={vaultCollection} onActivate={onVaultRequest} vault={vault} /> : null}
154
154
  </div>
155
155
  <ul aria-label="Vault readiness" className="vault-readiness-strip">
156
156
  {readiness.map((item) => <li key={item.label}><strong>{item.label}</strong>{item.value}</li>)}
@@ -175,7 +175,7 @@ export function VaultSurface({ status }: { status: GuiStatusData }) {
175
175
  <h2>{vault.readiness.setup_required ? "Setup required" : "Setup metadata"}</h2>
176
176
  <p>{vault.setup_hint}</p>
177
177
  </div>
178
- <button className="secondary-action vault-cta" disabled title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
178
+ <button className="secondary-action vault-cta" onClick={() => onVaultRequest(vaultDialogIntentForStatus(vault))} title={vault.unlock_hint} type="button">{vault.unlocked ? "Lock Vault" : text.vaultUnlockCta}</button>
179
179
  </div>
180
180
  <ol className="vault-step-list">
181
181
  <li>Initialize locally with the hidden-prompt helper.</li>
@@ -191,7 +191,7 @@ export function VaultSurface({ status }: { status: GuiStatusData }) {
191
191
  <p>{text.vaultCollectionIntro}</p>
192
192
  </div>
193
193
  <ul className="object-list vault-collection-list">
194
- {vault.collections.map((collection) => <VaultCollectionRow collection={collection} key={collection.id} vault={vault} />)}
194
+ {vault.collections.map((collection) => <VaultCollectionRow collection={collection} key={collection.id} onVaultRequest={onVaultRequest} vault={vault} />)}
195
195
  </ul>
196
196
  </section>
197
197
  <section className="panel" aria-label="Vault devices and audit">
@@ -219,9 +219,10 @@ export function VaultSurface({ status }: { status: GuiStatusData }) {
219
219
  );
220
220
  }
221
221
 
222
- export function LockedVaultGate({ collection, label, vault }: {
222
+ export function LockedVaultGate({ collection, label, onVaultRequest, vault }: {
223
223
  collection: GuiVaultCollectionSummary;
224
224
  label: string;
225
+ onVaultRequest: (intent: VaultDialogIntent) => void;
225
226
  vault: GuiVaultStatusData;
226
227
  }) {
227
228
  return (
@@ -232,12 +233,12 @@ export function LockedVaultGate({ collection, label, vault }: {
232
233
  <h2>{label} is locked</h2>
233
234
  <p>{text.vaultLockedPreview}</p>
234
235
  </div>
235
- <VaultPadlock collection={collection} vault={vault} />
236
+ <VaultPadlock collection={collection} onActivate={onVaultRequest} vault={vault} />
236
237
  </div>
237
238
  <div className="notice compact-notice" role="note">
238
239
  {text.vaultTooltip} {vault.unlock_hint}
239
240
  </div>
240
- <button className="secondary-action vault-cta" disabled title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
241
+ <button className="secondary-action vault-cta" onClick={() => onVaultRequest(vaultDialogIntentForStatus(vault))} title={vault.unlock_hint} type="button">{text.vaultUnlockCta}</button>
241
242
  </section>
242
243
  );
243
244
  }
@@ -252,11 +253,11 @@ function VaultFeatureCard({ detail, label, value }: { detail: string; label: str
252
253
  );
253
254
  }
254
255
 
255
- function VaultCollectionRow({ collection, vault }: { collection: GuiVaultCollectionSummary; vault: GuiVaultStatusData }) {
256
+ function VaultCollectionRow({ collection, onVaultRequest, vault }: { collection: GuiVaultCollectionSummary; onVaultRequest: (intent: VaultDialogIntent) => void; vault: GuiVaultStatusData }) {
256
257
  return (
257
258
  <li>
258
259
  <strong>{collection.label}</strong>
259
- <VaultPadlock collection={collection} compact vault={vault} />
260
+ <VaultPadlock collection={collection} compact onActivate={onVaultRequest} vault={vault} />
260
261
  <span>{collection.preview_policy}</span>
261
262
  <small>{collection.labels.join(", ")}</small>
262
263
  <small>{collection.surface_ids.join(", ")}</small>
@@ -1,4 +1,5 @@
1
1
  import type { GuiVaultCollectionSummary, GuiVaultStatusData } from "@aidevops/gui-shared";
2
+ import type { KeyboardEvent, MouseEvent } from "react";
2
3
  import { FiLock, FiUnlock } from "react-icons/fi";
3
4
  import type { SurfaceId } from "./app-model";
4
5
  import { text } from "./app-model";
@@ -23,20 +24,53 @@ export function vaultCollectionTooltip(collection: GuiVaultCollectionSummary): s
23
24
  return text.vaultTooltip;
24
25
  }
25
26
 
26
- export function VaultPadlock({ collection, compact = false, vault }: {
27
+ export type VaultDialogIntent = "setup" | "unlock" | "lock";
28
+
29
+ export function vaultDialogIntentForStatus(vault: GuiVaultStatusData): VaultDialogIntent {
30
+ if (vault.unlocked) {
31
+ return "lock";
32
+ }
33
+
34
+ return vault.readiness.setup_required || !vault.initialized ? "setup" : "unlock";
35
+ }
36
+
37
+ export function VaultPadlock({ collection, compact = false, onActivate, vault }: {
27
38
  collection: GuiVaultCollectionSummary;
28
39
  compact?: boolean;
40
+ onActivate?: (intent: VaultDialogIntent) => void;
29
41
  vault: GuiVaultStatusData;
30
42
  }) {
31
43
  const locked = collection.state !== "unlocked" || !vault.unlocked;
32
44
  const stateLabel = locked ? "Locked" : "Unlocked";
33
45
  const Icon = locked ? FiLock : FiUnlock;
34
46
  const tooltip = `${stateLabel}: ${vaultCollectionTooltip(collection)}`;
47
+ const className = compact ? "vault-padlock compact" : "vault-padlock";
48
+
49
+ const activate = (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => {
50
+ if (onActivate === undefined) {
51
+ return;
52
+ }
53
+
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ onActivate(vaultDialogIntentForStatus(vault));
57
+ };
58
+
59
+ const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
60
+ if (event.key === "Enter" || event.key === " ") {
61
+ activate(event);
62
+ }
63
+ };
35
64
 
36
65
  return (
37
66
  <span
38
- className={compact ? "vault-padlock compact" : "vault-padlock"}
67
+ aria-label={tooltip}
68
+ className={onActivate ? `${className} interactive` : className}
39
69
  data-vault-state={locked ? "locked" : "unlocked"}
70
+ onClick={activate}
71
+ onKeyDown={handleKeyDown}
72
+ role={onActivate ? "button" : undefined}
73
+ tabIndex={onActivate ? 0 : undefined}
40
74
  title={tooltip}
41
75
  >
42
76
  <Icon aria-hidden="true" focusable="false" />
@@ -0,0 +1,132 @@
1
+ import { useEffect, useMemo, useState, type ReactElement } from "react";
2
+ import { FiAlertTriangle, FiCheckCircle, FiLock, FiShield, FiUnlock } from "react-icons/fi";
3
+ import type { GuiVaultStatusData } from "@aidevops/gui-shared";
4
+ import type { VaultDialogIntent } from "./VaultBadges";
5
+
6
+ export function VaultPassphraseModal({ intent, onClose, vault }: {
7
+ intent: VaultDialogIntent;
8
+ onClose: () => void;
9
+ vault: GuiVaultStatusData;
10
+ }): ReactElement {
11
+ const [step, setStep] = useState(0);
12
+ const [passphrase, setPassphrase] = useState("");
13
+ const [passphraseConfirm, setPassphraseConfirm] = useState("");
14
+ const [unlockPassphrase, setUnlockPassphrase] = useState("");
15
+ const [savedWarningAccepted, setSavedWarningAccepted] = useState(false);
16
+ const setupMode = intent === "setup";
17
+ const title = setupMode ? "Set Vault encryption passphrase" : intent === "unlock" ? "Unlock Vault" : "Lock Vault";
18
+ const actionLabel = setupMode ? "Use passphrase and continue" : intent === "unlock" ? "Unlock Vault" : "Lock Vault";
19
+ const setupPassphrasesMatch = passphrase.length > 0 && passphrase === passphraseConfirm;
20
+ const finalPassphraseMatches = unlockPassphrase.length > 0 && unlockPassphrase === passphrase;
21
+ const canContinue = useMemo(() => {
22
+ if (!setupMode) {
23
+ return intent === "lock" || unlockPassphrase.length > 0;
24
+ }
25
+
26
+ if (step === 0) {
27
+ return setupPassphrasesMatch;
28
+ }
29
+ if (step === 1) {
30
+ return savedWarningAccepted;
31
+ }
32
+ return finalPassphraseMatches;
33
+ }, [finalPassphraseMatches, intent, savedWarningAccepted, setupMode, setupPassphrasesMatch, step, unlockPassphrase.length]);
34
+
35
+ useEffect(() => {
36
+ setStep(0);
37
+ setPassphrase("");
38
+ setPassphraseConfirm("");
39
+ setUnlockPassphrase("");
40
+ setSavedWarningAccepted(false);
41
+ }, [intent]);
42
+
43
+ const continueFlow = () => {
44
+ if (setupMode && step < 2) {
45
+ setStep((current) => current + 1);
46
+ return;
47
+ }
48
+
49
+ onClose();
50
+ };
51
+
52
+ return (
53
+ <div className="vault-modal-backdrop" role="presentation">
54
+ <section aria-label={title} aria-modal="true" className="vault-modal" role="dialog">
55
+ <header className="vault-modal-header">
56
+ <span className="vault-modal-icon" aria-hidden="true">{intent === "lock" ? <FiLock /> : <FiUnlock />}</span>
57
+ <div>
58
+ <p className="eyebrow">aidevops Vault</p>
59
+ <h2>{title}</h2>
60
+ </div>
61
+ </header>
62
+ {setupMode ? <SetupStep accepted={savedWarningAccepted} confirm={passphraseConfirm} match={setupPassphrasesMatch} onAcceptChange={setSavedWarningAccepted} onConfirmChange={setPassphraseConfirm} onPassphraseChange={setPassphrase} onUnlockPassphraseChange={setUnlockPassphrase} passphrase={passphrase} step={step} unlockMatch={finalPassphraseMatches} unlockPassphrase={unlockPassphrase} /> : <UnlockStep intent={intent} onUnlockPassphraseChange={setUnlockPassphrase} unlockPassphrase={unlockPassphrase} vault={vault} />}
63
+ <footer className="vault-modal-actions">
64
+ <button className="secondary-action" onClick={onClose} type="button">Cancel</button>
65
+ <button className="primary-action" disabled={!canContinue} onClick={continueFlow} type="button">{setupMode && step < 2 ? "Continue" : actionLabel}</button>
66
+ </footer>
67
+ <p className="vault-modal-footnote">Passphrases stay in this local dialog state only and are never shown in logs, issues, command arguments, or AI chat.</p>
68
+ </section>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function SetupStep({ accepted, confirm, match, onAcceptChange, onConfirmChange, onPassphraseChange, onUnlockPassphraseChange, passphrase, step, unlockMatch, unlockPassphrase }: {
74
+ accepted: boolean;
75
+ confirm: string;
76
+ match: boolean;
77
+ onAcceptChange: (accepted: boolean) => void;
78
+ onConfirmChange: (value: string) => void;
79
+ onPassphraseChange: (value: string) => void;
80
+ onUnlockPassphraseChange: (value: string) => void;
81
+ passphrase: string;
82
+ step: number;
83
+ unlockMatch: boolean;
84
+ unlockPassphrase: string;
85
+ }): ReactElement {
86
+ if (step === 1) {
87
+ return (
88
+ <div className="vault-modal-body">
89
+ <div className="notice warning-notice" role="alert"><FiAlertTriangle aria-hidden="true" /> Save this passphrase in a password manager now. aidevops cannot recover encrypted data if it is lost.</div>
90
+ <label className="vault-confirm-check"><input checked={accepted} onChange={(event) => onAcceptChange(event.currentTarget.checked)} type="checkbox" /> I have saved the Vault passphrase in a password manager and understand recovery is not possible without it.</label>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ if (step === 2) {
96
+ return (
97
+ <div className="vault-modal-body">
98
+ <p>Final safety check: enter the newly saved passphrase once more before any confidential app data is encrypted.</p>
99
+ <label className="vault-field"><span>Use saved passphrase</span><input autoComplete="current-password" onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
100
+ {unlockPassphrase.length > 0 ? <p className={unlockMatch ? "vault-valid" : "vault-invalid"}>{unlockMatch ? "Passphrase matches." : "Passphrase does not match the saved setup value."}</p> : null}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <div className="vault-modal-body">
107
+ <p>Create the encryption passphrase for this local Vault. Use a long unique value generated by your password manager.</p>
108
+ <label className="vault-field"><span>New passphrase</span><input autoComplete="new-password" autoFocus onChange={(event) => onPassphraseChange(event.currentTarget.value)} type="password" value={passphrase} /></label>
109
+ <label className="vault-field"><span>Repeat passphrase</span><input autoComplete="new-password" onChange={(event) => onConfirmChange(event.currentTarget.value)} type="password" value={confirm} /></label>
110
+ {confirm.length > 0 ? <p className={match ? "vault-valid" : "vault-invalid"}>{match ? "Passphrases match." : "Passphrases must match before continuing."}</p> : null}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ function UnlockStep({ intent, onUnlockPassphraseChange, unlockPassphrase, vault }: {
116
+ intent: VaultDialogIntent;
117
+ onUnlockPassphraseChange: (value: string) => void;
118
+ unlockPassphrase: string;
119
+ vault: GuiVaultStatusData;
120
+ }): ReactElement {
121
+ if (intent === "lock") {
122
+ return <div className="vault-modal-body"><p><FiLock aria-hidden="true" /> Locking forgets in-memory keys for this local session. Protected previews will be hidden again.</p></div>;
123
+ }
124
+
125
+ return (
126
+ <div className="vault-modal-body">
127
+ <p><FiShield aria-hidden="true" /> Enter your Vault passphrase to unlock protected previews and write actions for this local session.</p>
128
+ <label className="vault-field"><span>Vault passphrase</span><input autoComplete="current-password" autoFocus onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
129
+ <div className="notice compact-notice" role="note"><FiCheckCircle aria-hidden="true" /> {vault.unlock_hint}</div>
130
+ </div>
131
+ );
132
+ }
@@ -953,6 +953,15 @@ code {
953
953
  color: var(--accent);
954
954
  }
955
955
 
956
+ .vault-padlock.interactive {
957
+ cursor: pointer;
958
+ }
959
+
960
+ .vault-padlock.interactive:focus-visible {
961
+ outline: 2px solid var(--accent);
962
+ outline-offset: 2px;
963
+ }
964
+
956
965
  .sidebar-group h2 {
957
966
  font-size: max(12.5px, 0.84em);
958
967
  }
@@ -1605,6 +1614,130 @@ code {
1605
1614
  width: 100%;
1606
1615
  }
1607
1616
 
1617
+ .vault-modal-backdrop {
1618
+ align-items: center;
1619
+ backdrop-filter: blur(10px) saturate(130%);
1620
+ background: rgb(0 0 0 / 46%);
1621
+ display: flex;
1622
+ inset: 0;
1623
+ justify-content: center;
1624
+ padding: 24px;
1625
+ position: fixed;
1626
+ z-index: 30;
1627
+ -webkit-backdrop-filter: blur(10px) saturate(130%);
1628
+ }
1629
+
1630
+ .vault-modal {
1631
+ background: var(--surface-elevated);
1632
+ border: 1px solid var(--border-accent);
1633
+ border-radius: 24px;
1634
+ box-shadow: 0 30px 90px rgb(0 0 0 / 42%);
1635
+ display: grid;
1636
+ gap: 18px;
1637
+ max-width: min(620px, calc(100vw - 40px));
1638
+ padding: 22px;
1639
+ width: 100%;
1640
+ }
1641
+
1642
+ .vault-modal-header,
1643
+ .vault-modal-actions,
1644
+ .vault-modal-body p,
1645
+ .vault-field,
1646
+ .vault-confirm-check,
1647
+ .vault-valid,
1648
+ .vault-invalid {
1649
+ margin: 0;
1650
+ }
1651
+
1652
+ .vault-modal-header {
1653
+ align-items: center;
1654
+ display: flex;
1655
+ gap: 14px;
1656
+ }
1657
+
1658
+ .vault-modal-icon {
1659
+ align-items: center;
1660
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
1661
+ border: 1px solid var(--border-accent);
1662
+ border-radius: 16px;
1663
+ color: var(--accent);
1664
+ display: inline-flex;
1665
+ height: 46px;
1666
+ justify-content: center;
1667
+ width: 46px;
1668
+ }
1669
+
1670
+ .vault-modal-body {
1671
+ display: grid;
1672
+ gap: 14px;
1673
+ }
1674
+
1675
+ .vault-field {
1676
+ display: grid;
1677
+ gap: 7px;
1678
+ }
1679
+
1680
+ .vault-field span,
1681
+ .vault-confirm-check {
1682
+ color: var(--text-secondary);
1683
+ font-size: 0.9rem;
1684
+ font-weight: 800;
1685
+ }
1686
+
1687
+ .vault-field input {
1688
+ background: var(--surface-muted);
1689
+ border: 1px solid var(--border);
1690
+ border-radius: 12px;
1691
+ color: var(--text-primary);
1692
+ padding: 12px;
1693
+ }
1694
+
1695
+ .vault-confirm-check {
1696
+ align-items: flex-start;
1697
+ display: flex;
1698
+ gap: 10px;
1699
+ }
1700
+
1701
+ .warning-notice {
1702
+ border-color: var(--border-warning);
1703
+ color: #b45309;
1704
+ }
1705
+
1706
+ .vault-valid {
1707
+ color: var(--accent);
1708
+ font-weight: 800;
1709
+ }
1710
+
1711
+ .vault-invalid {
1712
+ color: #b45309;
1713
+ font-weight: 800;
1714
+ }
1715
+
1716
+ .vault-modal-actions {
1717
+ display: flex;
1718
+ gap: 10px;
1719
+ justify-content: flex-end;
1720
+ }
1721
+
1722
+ .primary-action {
1723
+ background: var(--accent);
1724
+ border: 1px solid var(--border-accent);
1725
+ border-radius: 10px;
1726
+ color: var(--surface-base);
1727
+ font-weight: 900;
1728
+ }
1729
+
1730
+ .primary-action:disabled {
1731
+ cursor: not-allowed;
1732
+ opacity: 0.5;
1733
+ }
1734
+
1735
+ .vault-modal-footnote {
1736
+ color: var(--text-muted);
1737
+ font-size: 0.82rem;
1738
+ margin: 0;
1739
+ }
1740
+
1608
1741
  .command-input-row {
1609
1742
  align-items: center;
1610
1743
  background: var(--surface-muted);
@@ -1659,6 +1792,8 @@ code {
1659
1792
  }
1660
1793
 
1661
1794
  .workspace-scroll {
1795
+ align-content: start;
1796
+ align-items: start;
1662
1797
  display: grid;
1663
1798
  gap: 16px;
1664
1799
  overflow-x: hidden;
@@ -1667,6 +1802,7 @@ code {
1667
1802
  }
1668
1803
 
1669
1804
  .surface-page {
1805
+ align-content: start;
1670
1806
  display: grid;
1671
1807
  gap: 16px;
1672
1808
  }
@@ -1973,17 +2109,21 @@ code {
1973
2109
 
1974
2110
  .settings-surface,
1975
2111
  .settings-form {
2112
+ align-content: start;
1976
2113
  display: grid;
1977
2114
  gap: 16px;
1978
2115
  }
1979
2116
 
1980
2117
  .settings-form {
2118
+ align-items: start;
1981
2119
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1982
2120
  }
1983
2121
 
1984
2122
  .settings-form label {
2123
+ align-content: start;
1985
2124
  display: grid;
1986
2125
  gap: 8px;
2126
+ min-height: 0;
1987
2127
  }
1988
2128
 
1989
2129
  .settings-form span {
@@ -1997,7 +2137,9 @@ code {
1997
2137
  border: 1px solid var(--border);
1998
2138
  border-radius: 14px;
1999
2139
  color: var(--text-secondary);
2140
+ min-height: 44px;
2000
2141
  padding: 11px 12px;
2142
+ width: 100%;
2001
2143
  }
2002
2144
 
2003
2145
  .notification-list li {
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.31.80
15
+ # Version: 3.32.0
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)