aidevops 3.31.81 → 3.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.81-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
63
+ [![Version](https://img.shields.io/badge/Version-3.32.1-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.81
1
+ 3.32.1
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.81
8
+ # Version: 3.32.1
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.81",
3
+ "version": "3.32.1",
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
  {
@@ -1,5 +1,5 @@
1
1
  import type { GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
2
- import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactElement, useEffect, useState } from "react";
2
+ import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactElement, useEffect, useRef, useState } from "react";
3
3
  import { MachineRail, Sidebar } from "./AppNavigation";
4
4
  import { Workspace } from "./AppWorkspace";
5
5
  import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
@@ -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,8 @@ 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);
111
+ const hasPromptedVaultSetup = useRef(false);
108
112
  const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
109
113
  const resolvedTheme = themePreference === "system" ? systemTheme : themePreference;
110
114
  const activeSurface: SurfaceId = navigation.entries[navigation.index] ?? "overview";
@@ -209,6 +213,13 @@ export function App(): ReactElement {
209
213
  setSelectedLocalRepoIndex((current) => Math.min(current, Math.max(0, status.data.local_repos.repos.length - 1)));
210
214
  }, [status.data.local_repos.repos.length]);
211
215
 
216
+ useEffect(() => {
217
+ if (shouldPromptVaultSetup(statusLoading, status.data.vault.readiness.setup_required, hasPromptedVaultSetup.current)) {
218
+ hasPromptedVaultSetup.current = true;
219
+ setVaultDialogIntent("setup");
220
+ }
221
+ }, [status.data.vault.readiness.setup_required, statusLoading]);
222
+
212
223
  return (
213
224
  <main className={machineRailVisible ? "app-shell" : "app-shell machine-rail-collapsed"} aria-busy={statusLoading}>
214
225
  <div className="desktop-titlebar-tagline" aria-hidden="true">Your data protected. Your systems managed. Your creations published.</div>
@@ -221,6 +232,7 @@ export function App(): ReactElement {
221
232
  conversationMode={conversationMode}
222
233
  fontSizePreference={fontSizePreference}
223
234
  fontPreference={fontPreference}
235
+ onVaultRequest={(intent) => setVaultDialogIntent(intent)}
224
236
  selectedLocalRepoIndex={selectedLocalRepoIndex}
225
237
  selectedSessionId={selectedSessionId}
226
238
  setAccentHue={setAccentHue}
@@ -252,6 +264,7 @@ export function App(): ReactElement {
252
264
  fileRoot={fileRoot}
253
265
  goBack={goBack}
254
266
  goForward={goForward}
267
+ onVaultRequest={(intent) => setVaultDialogIntent(intent)}
255
268
  selectedLocalRepoIndex={selectedLocalRepoIndex}
256
269
  selectedSessionId={selectedSessionId}
257
270
  setActiveSurface={setActiveSurface}
@@ -263,6 +276,7 @@ export function App(): ReactElement {
263
276
  status={status.data}
264
277
  />
265
278
  <DesktopStatusBar status={status.data} />
279
+ {vaultDialogIntent ? <VaultPassphraseModal intent={vaultDialogIntent} onClose={() => setVaultDialogIntent(null)} vault={status.data.vault} /> : null}
266
280
  </main>
267
281
  );
268
282
  }
@@ -331,6 +345,10 @@ export function clampSidebarWidth(width: number): number {
331
345
  return Math.min(maxSidebarWidth, Math.max(minSidebarWidth, Math.round(width)));
332
346
  }
333
347
 
348
+ export function shouldPromptVaultSetup(statusLoading: boolean, setupRequired: boolean, hasPromptedVaultSetup: boolean): boolean {
349
+ return !statusLoading && setupRequired && !hasPromptedVaultSetup;
350
+ }
351
+
334
352
  function SidebarResizeHandle(): ReactElement {
335
353
  const [sidebarWidth, setSidebarWidth] = useState(() => readStoredSidebarWidth());
336
354
 
@@ -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
  >
@@ -359,8 +369,8 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
359
369
  {shouldShowCount ? <span className="surface-count">({recordCount})</span> : null}
360
370
  </span>
361
371
  </span>
362
- {vaultCollection ? <VaultPadlock collection={vaultCollection} compact vault={status.vault} /> : null}
363
372
  {item.badge ? <em>{item.badge}</em> : null}
373
+ {vaultCollection ? <VaultPadlock collection={vaultCollection} compact vault={status.vault} /> : null}
364
374
  </button>
365
375
  </li>
366
376
  );
@@ -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,143 @@
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
+ <form
55
+ aria-label={title}
56
+ aria-modal="true"
57
+ className="vault-modal"
58
+ onSubmit={(event) => {
59
+ event.preventDefault();
60
+ if (canContinue) {
61
+ continueFlow();
62
+ }
63
+ }}
64
+ role="dialog"
65
+ >
66
+ <header className="vault-modal-header">
67
+ <span className="vault-modal-icon" aria-hidden="true">{intent === "lock" ? <FiLock /> : <FiUnlock />}</span>
68
+ <div>
69
+ <p className="eyebrow">aidevops Vault</p>
70
+ <h2>{title}</h2>
71
+ </div>
72
+ </header>
73
+ {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} />}
74
+ <footer className="vault-modal-actions">
75
+ <button className="secondary-action" onClick={onClose} type="button">Cancel</button>
76
+ <button className="primary-action" disabled={!canContinue} type="submit">{setupMode && step < 2 ? "Continue" : actionLabel}</button>
77
+ </footer>
78
+ <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>
79
+ </form>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ function SetupStep({ accepted, confirm, match, onAcceptChange, onConfirmChange, onPassphraseChange, onUnlockPassphraseChange, passphrase, step, unlockMatch, unlockPassphrase }: {
85
+ accepted: boolean;
86
+ confirm: string;
87
+ match: boolean;
88
+ onAcceptChange: (accepted: boolean) => void;
89
+ onConfirmChange: (value: string) => void;
90
+ onPassphraseChange: (value: string) => void;
91
+ onUnlockPassphraseChange: (value: string) => void;
92
+ passphrase: string;
93
+ step: number;
94
+ unlockMatch: boolean;
95
+ unlockPassphrase: string;
96
+ }): ReactElement {
97
+ if (step === 1) {
98
+ return (
99
+ <div className="vault-modal-body">
100
+ <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>
101
+ <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>
102
+ </div>
103
+ );
104
+ }
105
+
106
+ if (step === 2) {
107
+ return (
108
+ <div className="vault-modal-body">
109
+ <p>Final safety check: enter the newly saved passphrase once more before any confidential app data is encrypted.</p>
110
+ <label className="vault-field"><span>Use saved passphrase</span><input autoComplete="current-password" onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
111
+ {unlockPassphrase.length > 0 ? <p className={unlockMatch ? "vault-valid" : "vault-invalid"}>{unlockMatch ? "Passphrase matches." : "Passphrase does not match the saved setup value."}</p> : null}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <div className="vault-modal-body">
118
+ <p>Create the encryption passphrase for this local Vault. Use a long unique value generated by your password manager.</p>
119
+ <label className="vault-field"><span>New passphrase</span><input autoComplete="new-password" autoFocus onChange={(event) => onPassphraseChange(event.currentTarget.value)} type="password" value={passphrase} /></label>
120
+ <label className="vault-field"><span>Repeat passphrase</span><input autoComplete="new-password" onChange={(event) => onConfirmChange(event.currentTarget.value)} type="password" value={confirm} /></label>
121
+ {confirm.length > 0 ? <p className={match ? "vault-valid" : "vault-invalid"}>{match ? "Passphrases match." : "Passphrases must match before continuing."}</p> : null}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function UnlockStep({ intent, onUnlockPassphraseChange, unlockPassphrase, vault }: {
127
+ intent: VaultDialogIntent;
128
+ onUnlockPassphraseChange: (value: string) => void;
129
+ unlockPassphrase: string;
130
+ vault: GuiVaultStatusData;
131
+ }): ReactElement {
132
+ if (intent === "lock") {
133
+ 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>;
134
+ }
135
+
136
+ return (
137
+ <div className="vault-modal-body">
138
+ <p><FiShield aria-hidden="true" /> Enter your Vault passphrase to unlock protected previews and write actions for this local session.</p>
139
+ <label className="vault-field"><span>Vault passphrase</span><input autoComplete="current-password" autoFocus onChange={(event) => onUnlockPassphraseChange(event.currentTarget.value)} type="password" value={unlockPassphrase} /></label>
140
+ <div className="notice compact-notice" role="note"><FiCheckCircle aria-hidden="true" /> {vault.unlock_hint}</div>
141
+ </div>
142
+ );
143
+ }
@@ -239,10 +239,65 @@ body {
239
239
 
240
240
  button,
241
241
  input,
242
- select {
242
+ select,
243
+ textarea {
243
244
  font: inherit;
244
245
  }
245
246
 
247
+ :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea) {
248
+ background:
249
+ linear-gradient(180deg, rgb(255 255 255 / 12%), rgb(255 255 255 / 0%)),
250
+ color-mix(in srgb, var(--surface-muted) 88%, var(--surface-elevated));
251
+ border: 1px solid color-mix(in srgb, var(--border) 86%, var(--surface-elevated));
252
+ border-radius: 12px;
253
+ box-shadow:
254
+ inset 0 1px 0 rgb(255 255 255 / 18%),
255
+ inset 0 -1px 0 rgb(0 0 0 / 8%),
256
+ 0 1px 2px rgb(0 0 0 / 8%);
257
+ color: var(--text-primary);
258
+ min-height: 38px;
259
+ padding: 8px 12px;
260
+ transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease, color 140ms ease;
261
+ }
262
+
263
+ :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea)::placeholder {
264
+ color: color-mix(in srgb, var(--text-muted) 78%, transparent);
265
+ }
266
+
267
+ :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea):hover:not(:disabled) {
268
+ border-color: var(--border-hover);
269
+ }
270
+
271
+ :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea):focus-visible {
272
+ border-color: color-mix(in srgb, var(--accent) 58%, var(--border));
273
+ box-shadow:
274
+ 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent),
275
+ inset 0 1px 0 rgb(255 255 255 / 22%),
276
+ 0 1px 2px rgb(0 0 0 / 10%);
277
+ outline: none;
278
+ }
279
+
280
+ :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]):not([type="color"]), select, textarea):disabled {
281
+ color: var(--text-secondary);
282
+ cursor: not-allowed;
283
+ opacity: 0.9;
284
+ }
285
+
286
+ select {
287
+ appearance: none;
288
+ background-image:
289
+ linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
290
+ linear-gradient(135deg, var(--text-muted) 50%, transparent 50%),
291
+ linear-gradient(180deg, rgb(255 255 255 / 12%), rgb(255 255 255 / 0%));
292
+ background-position:
293
+ calc(100% - 16px) 50%,
294
+ calc(100% - 11px) 50%,
295
+ 0 0;
296
+ background-repeat: no-repeat;
297
+ background-size: 5px 5px, 5px 5px, 100% 100%;
298
+ padding-right: 32px;
299
+ }
300
+
246
301
  button {
247
302
  cursor: pointer;
248
303
  }
@@ -953,6 +1008,15 @@ code {
953
1008
  color: var(--accent);
954
1009
  }
955
1010
 
1011
+ .vault-padlock.interactive {
1012
+ cursor: pointer;
1013
+ }
1014
+
1015
+ .vault-padlock.interactive:focus-visible {
1016
+ outline: 2px solid var(--accent);
1017
+ outline-offset: 2px;
1018
+ }
1019
+
956
1020
  .sidebar-group h2 {
957
1021
  font-size: max(12.5px, 0.84em);
958
1022
  }
@@ -1605,6 +1669,130 @@ code {
1605
1669
  width: 100%;
1606
1670
  }
1607
1671
 
1672
+ .vault-modal-backdrop {
1673
+ align-items: center;
1674
+ backdrop-filter: blur(10px) saturate(130%);
1675
+ background: rgb(0 0 0 / 46%);
1676
+ display: flex;
1677
+ inset: 0;
1678
+ justify-content: center;
1679
+ padding: 24px;
1680
+ position: fixed;
1681
+ z-index: 30;
1682
+ -webkit-backdrop-filter: blur(10px) saturate(130%);
1683
+ }
1684
+
1685
+ .vault-modal {
1686
+ background: var(--surface-elevated);
1687
+ border: 1px solid var(--border-accent);
1688
+ border-radius: 24px;
1689
+ box-shadow: 0 30px 90px rgb(0 0 0 / 42%);
1690
+ display: grid;
1691
+ gap: 18px;
1692
+ max-width: min(620px, calc(100vw - 40px));
1693
+ padding: 22px;
1694
+ width: 100%;
1695
+ }
1696
+
1697
+ .vault-modal-header,
1698
+ .vault-modal-actions,
1699
+ .vault-modal-body p,
1700
+ .vault-field,
1701
+ .vault-confirm-check,
1702
+ .vault-valid,
1703
+ .vault-invalid {
1704
+ margin: 0;
1705
+ }
1706
+
1707
+ .vault-modal-header {
1708
+ align-items: center;
1709
+ display: flex;
1710
+ gap: 14px;
1711
+ }
1712
+
1713
+ .vault-modal-icon {
1714
+ align-items: center;
1715
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
1716
+ border: 1px solid var(--border-accent);
1717
+ border-radius: 16px;
1718
+ color: var(--accent);
1719
+ display: inline-flex;
1720
+ height: 46px;
1721
+ justify-content: center;
1722
+ width: 46px;
1723
+ }
1724
+
1725
+ .vault-modal-body {
1726
+ display: grid;
1727
+ gap: 14px;
1728
+ }
1729
+
1730
+ .vault-field {
1731
+ display: grid;
1732
+ gap: 7px;
1733
+ }
1734
+
1735
+ .vault-field span,
1736
+ .vault-confirm-check {
1737
+ color: var(--text-secondary);
1738
+ font-size: 0.9rem;
1739
+ font-weight: 800;
1740
+ }
1741
+
1742
+ .vault-field input {
1743
+ background: var(--surface-muted);
1744
+ border: 1px solid var(--border);
1745
+ border-radius: 12px;
1746
+ color: var(--text-primary);
1747
+ padding: 12px;
1748
+ }
1749
+
1750
+ .vault-confirm-check {
1751
+ align-items: flex-start;
1752
+ display: flex;
1753
+ gap: 10px;
1754
+ }
1755
+
1756
+ .warning-notice {
1757
+ border-color: var(--border-warning);
1758
+ color: #b45309;
1759
+ }
1760
+
1761
+ .vault-valid {
1762
+ color: var(--accent);
1763
+ font-weight: 800;
1764
+ }
1765
+
1766
+ .vault-invalid {
1767
+ color: #b45309;
1768
+ font-weight: 800;
1769
+ }
1770
+
1771
+ .vault-modal-actions {
1772
+ display: flex;
1773
+ gap: 10px;
1774
+ justify-content: flex-end;
1775
+ }
1776
+
1777
+ .primary-action {
1778
+ background: var(--accent);
1779
+ border: 1px solid var(--border-accent);
1780
+ border-radius: 10px;
1781
+ color: var(--surface-base);
1782
+ font-weight: 900;
1783
+ }
1784
+
1785
+ .primary-action:disabled {
1786
+ cursor: not-allowed;
1787
+ opacity: 0.5;
1788
+ }
1789
+
1790
+ .vault-modal-footnote {
1791
+ color: var(--text-muted);
1792
+ font-size: 0.82rem;
1793
+ margin: 0;
1794
+ }
1795
+
1608
1796
  .command-input-row {
1609
1797
  align-items: center;
1610
1798
  background: var(--surface-muted);
@@ -1659,6 +1847,8 @@ code {
1659
1847
  }
1660
1848
 
1661
1849
  .workspace-scroll {
1850
+ align-content: start;
1851
+ align-items: start;
1662
1852
  display: grid;
1663
1853
  gap: 16px;
1664
1854
  overflow-x: hidden;
@@ -1667,6 +1857,7 @@ code {
1667
1857
  }
1668
1858
 
1669
1859
  .surface-page {
1860
+ align-content: start;
1670
1861
  display: grid;
1671
1862
  gap: 16px;
1672
1863
  }
@@ -1973,17 +2164,21 @@ code {
1973
2164
 
1974
2165
  .settings-surface,
1975
2166
  .settings-form {
2167
+ align-content: start;
1976
2168
  display: grid;
1977
2169
  gap: 16px;
1978
2170
  }
1979
2171
 
1980
2172
  .settings-form {
2173
+ align-items: start;
1981
2174
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1982
2175
  }
1983
2176
 
1984
2177
  .settings-form label {
2178
+ align-content: start;
1985
2179
  display: grid;
1986
2180
  gap: 8px;
2181
+ min-height: 0;
1987
2182
  }
1988
2183
 
1989
2184
  .settings-form span {
@@ -1997,7 +2192,9 @@ code {
1997
2192
  border: 1px solid var(--border);
1998
2193
  border-radius: 14px;
1999
2194
  color: var(--text-secondary);
2195
+ min-height: 44px;
2000
2196
  padding: 11px 12px;
2197
+ width: 100%;
2001
2198
  }
2002
2199
 
2003
2200
  .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.81
15
+ # Version: 3.32.1
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)