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 +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-shared/src/fixtures.ts +45 -1
- package/packages/gui-web/src/App.tsx +19 -1
- package/packages/gui-web/src/AppNavigation.tsx +19 -9
- package/packages/gui-web/src/AppWorkspace.tsx +19 -11
- package/packages/gui-web/src/StatusSurfaces.tsx +11 -10
- package/packages/gui-web/src/VaultBadges.tsx +36 -2
- package/packages/gui-web/src/VaultPassphraseModal.tsx +143 -0
- package/packages/gui-web/src/styles.css +198 -1
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
60
60
|
[](https://github.com/marcusquinn)
|
|
61
61
|
|
|
62
62
|
<!-- Release & Version Info -->
|
|
63
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
64
64
|
[](https://www.npmjs.com/package/aidevops)
|
|
65
65
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
66
66
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.32.1
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -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"
|
|
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={() =>
|
|
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"
|
|
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"
|
|
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
|
|
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
|
-
|
|
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.
|
|
15
|
+
# Version: 3.32.1
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|