aidevops 3.32.4 → 3.32.6

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.32.4-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
63
+ [![Version](https://img.shields.io/badge/Version-3.32.6-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)
@@ -116,9 +116,9 @@ the required notices and preferred credit text.
116
116
 
117
117
  ### Agent Structure
118
118
 
119
- - 12 primary agents (Build+, Automate, Product, SEO, Marketing-Sales, etc.) with specialist @subagents on demand
120
- - 2,200+ agent and subagent markdown files organized by domain
121
- - 1,800+ helper scripts in `.agents/scripts/`
119
+ - 14 primary agents (Build+, Automate, Product, SEO, Marketing-Sales, etc.) with specialist @subagents on demand
120
+ - 2,150+ agent and subagent markdown files organized by domain
121
+ - 1,630+ helper scripts in `.agents/scripts/`
122
122
  - 185+ slash commands and workflow guides for common operations
123
123
 
124
124
  ### What You Can Ask aidevops To Do
@@ -782,9 +782,9 @@ aidevops implements proven agent design patterns identified by [Lance Martin (La
782
782
 
783
783
  | Pattern | Description | aidevops Implementation |
784
784
  |---------|-------------|------------------------|
785
- | **Give Agents a Computer** | Filesystem + shell for persistent context | `~/.aidevops/.agent-workspace/`, 1,480+ helper scripts |
785
+ | **Give Agents a Computer** | Filesystem + shell for persistent context | `~/.aidevops/.agent-workspace/`, 1,630+ helper scripts |
786
786
  | **Multi-Layer Action Space** | Few tools, push actions to computer | Per-agent MCP filtering (~12-20 tools each) |
787
- | **Knowledge Graph Routing** | Indexed, cross-referenced agents instead of isolated skills | `subagent-index.toon` maps 2,050+ agents by domain, purpose, and dependency — agents discover related context through the graph, not just their own file |
787
+ | **Knowledge Graph Routing** | Indexed, cross-referenced agents instead of isolated skills | `subagent-index.toon` maps 2,150+ agents by domain, purpose, and dependency — agents discover related context through the graph, not just their own file |
788
788
  | **Progressive Disclosure** | Load context on-demand | Subagent routing with content summaries, YAML frontmatter, read-on-demand |
789
789
  | **Offload Context** | Write results to filesystem | `.agent-workspace/work/[project]/` for persistence |
790
790
  | **Cache Context** | Prompt caching for cost | Stable instruction prefixes |
@@ -1907,7 +1907,7 @@ aidevops is registered as a **Claude Code plugin marketplace**. Install with two
1907
1907
  /plugin install aidevops@aidevops
1908
1908
  ```
1909
1909
 
1910
- This installs the complete framework: 12 primary agents, 2,050+ subagents, and 1,480+ helper scripts.
1910
+ This installs the complete framework: 14 primary agents, 2,150+ subagents, and 1,630+ helper scripts.
1911
1911
 
1912
1912
  ### Importing External Skills
1913
1913
 
@@ -1989,7 +1989,7 @@ Primary agents live at `.agents/<name>.md`. Each is a domain expert with its own
1989
1989
  | Research | `research.md` | Technical and market research, competitive analysis | gemini/grok |
1990
1990
  | SEO | `seo.md` | SEO audits, keyword research, GSC, schema, technical SEO | opus |
1991
1991
 
1992
- **Specialist subagents** (e.g. `@wordpress`, `@seo`, Build-Agent, Build-MCP, etc.) live under `.agents/tools/` or as `mode: subagent` files and are invoked via `@mention` when domain expertise is needed. See `subagent-index.toon` for the full roster.
1992
+ **Specialist subagents** (e.g. `@wordpress`, `@seo`, Build-Agent, Build-MCP, etc.) live under `.agents/tools/` or as `mode: subagent` files and are invoked via `@mention` when domain expertise is needed. See `subagent-index.toon` for the full roster. For read-only macOS Activity Monitor and background-efficiency audits, use `@macos-activity-cleaner` or `/aidevops-macos-activity-cleaner`.
1993
1993
 
1994
1994
  #### How to invoke a main agent
1995
1995
 
@@ -2037,7 +2037,7 @@ The long-term direction is to make slash commands and `@mentions` unnecessary al
2037
2037
 
2038
2038
  ### **Example Subagents with MCP Integration**
2039
2039
 
2040
- These are examples of subagents that have supporting MCPs enabled. See `.agents/` for the full list of 2,050+ subagents organized by domain.
2040
+ These are examples of subagents that have supporting MCPs enabled. See `.agents/` for the full list of 2,150+ subagents organized by domain.
2041
2041
 
2042
2042
  | Agent | Purpose | MCPs Enabled |
2043
2043
  |-------|---------|--------------|
@@ -2858,7 +2858,7 @@ See `.agents/tools/credentials/multi-tenant.md` for complete documentation.
2858
2858
  - Autonomous supervisor — pulse runs every 2 minutes, merging PRs, dispatching workers, killing stuck processes, advancing missions
2859
2859
  - Operational intelligence — struggle-ratio detection, orphaned PR recovery, circuit breaker, dynamic concurrency
2860
2860
  - Cost-aware routing — provider-aware model selection across OpenAI, Anthropic, Gemini, Cursor, Grok, and local models with budget tracking
2861
- - Progressive context — 2,050+ subagents loaded on demand, project bundles auto-configuring quality gates and model tiers
2861
+ - Progressive context — 2,150+ subagents loaded on demand, project bundles auto-configuring quality gates and model tiers
2862
2862
  - Self-improving — session mining extracts learnings, quality findings auto-create tasks, patterns feed back into agent prompts
2863
2863
 
2864
2864
  **Get Started:**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.32.4
1
+ 3.32.6
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.32.4
8
+ # Version: 3.32.6
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.32.4",
3
+ "version": "3.32.6",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -153,6 +153,7 @@ export function readStatus(
153
153
  notifications,
154
154
  vault,
155
155
  pulse_workers: pulseWorkers.summary,
156
+ secrets: vault.unlocked ? statusFixture.secrets : [],
156
157
  };
157
158
 
158
159
  const envelope = createEnvelope({
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import {
@@ -19,17 +19,43 @@ const GUI_VAULT_SETUP_STATES = [
19
19
  ] as const satisfies readonly GuiVaultSetupState[];
20
20
  const INITIALIZED_VAULT_STATUSES = new Set<GuiVaultStatus>(["locked", "unlocked", "corrupted"]);
21
21
  const RESTART_TEST_SETUP_STATES = new Set<GuiVaultSetupState>(["test-created", "restart-required", "test-verified"]);
22
+ type VaultCommand = "status" | "setup-state";
23
+ type VaultProbeValue = GuiVaultStatus | GuiVaultSetupState;
24
+ const COHERENT_VAULT_STATES: Readonly<Record<GuiVaultStatus, ReadonlySet<GuiVaultSetupState>>> = {
25
+ uninitialized: new Set(["uninitialized"]),
26
+ locked: new Set(["test-created", "restart-required", "test-verified", "migration-ready"]),
27
+ unlocked: new Set(["migration-ready"]),
28
+ corrupted: new Set(["unknown"]),
29
+ unknown: new Set(),
30
+ };
31
+ const EXPECTED_VAULT_EXIT_CODES: Readonly<
32
+ Record<VaultCommand, Readonly<Partial<Record<VaultProbeValue, number>>>>
33
+ > = {
34
+ status: { locked: 0, unlocked: 0, uninitialized: 2, corrupted: 3 },
35
+ "setup-state": {
36
+ "test-created": 0,
37
+ "restart-required": 0,
38
+ "test-verified": 0,
39
+ "migration-ready": 0,
40
+ uninitialized: 2,
41
+ unknown: 3,
42
+ },
43
+ };
22
44
 
23
45
  export function readVaultSummary(repoRoot: string): GuiVaultStatusData {
24
46
  const helperPath = join(repoRoot, ".agents", "scripts", "vault-helper.sh");
25
47
  const helperExists = existsSync(helperPath);
26
- const rawStatus = helperExists ? readVaultCommand(helperPath, ["status"], isGuiVaultStatus) : null;
27
- const rawSetupState = helperExists ? readVaultCommand(helperPath, ["setup-state"], isGuiVaultSetupState) : null;
48
+ const parsedStatus = helperExists ? readVaultCommand(helperPath, "status", isGuiVaultStatus) : null;
49
+ const parsedSetupState = helperExists ? readVaultCommand(helperPath, "setup-state", isGuiVaultSetupState) : null;
50
+ const coherent = coherentVaultState(parsedStatus, parsedSetupState);
51
+ const rawStatus = coherent ? parsedStatus : null;
52
+ const rawSetupState = coherent ? parsedSetupState : null;
28
53
  const helperStatus = vaultHelperStatus(helperExists, rawStatus, rawSetupState);
29
54
  const status = rawStatus ?? "unknown";
30
55
  const setupState = rawSetupState ?? fallbackSetupState(status);
31
56
  const unlocked = status === "unlocked";
32
57
  const collectionState = vaultCollectionState(status);
58
+ const setupRequired = helperStatus === "available" && status === "uninitialized" && setupState === "uninitialized";
33
59
 
34
60
  return {
35
61
  ...statusFixture.vault,
@@ -38,12 +64,12 @@ export function readVaultSummary(repoRoot: string): GuiVaultStatusData {
38
64
  initialized: INITIALIZED_VAULT_STATUSES.has(status),
39
65
  locked: !unlocked,
40
66
  unlocked,
41
- available: helperExists && helperStatus !== "error",
67
+ available: helperStatus === "available",
42
68
  helper_status: helperStatus,
43
69
  readiness: {
44
70
  ...statusFixture.vault.readiness,
45
71
  migration_allowed: unlocked && setupState === "migration-ready",
46
- setup_required: status === "uninitialized" || setupState === "uninitialized",
72
+ setup_required: setupRequired,
47
73
  restart_test_required: RESTART_TEST_SETUP_STATES.has(setupState),
48
74
  locked_content_hidden: !unlocked,
49
75
  },
@@ -54,22 +80,39 @@ export function readVaultSummary(repoRoot: string): GuiVaultStatusData {
54
80
  };
55
81
  }
56
82
 
57
- function readVaultCommand<T extends string>(
83
+ function readVaultCommand<T extends VaultProbeValue>(
58
84
  helperPath: string,
59
- args: string[],
85
+ command: VaultCommand,
60
86
  isAllowed: (value: string) => value is T,
61
87
  ): T | null {
62
- try {
63
- const output = execFileSync("sh", [helperPath, ...args], {
64
- encoding: "utf8",
65
- stdio: ["ignore", "pipe", "ignore"],
66
- timeout: 300,
67
- }).trim();
68
- const firstLine = output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "";
69
- return isAllowed(firstLine) ? firstLine : null;
70
- } catch {
71
- return null;
88
+ const result = spawnSync("/bin/bash", [helperPath, command], {
89
+ encoding: "utf8",
90
+ env: vaultProbeEnvironment(),
91
+ stdio: ["ignore", "pipe", "pipe"],
92
+ timeout: 750,
93
+ });
94
+ if (result.error !== undefined || result.signal !== null || result.status === null || result.stderr.length > 0) return null;
95
+ const output = result.stdout.endsWith("\n") ? result.stdout.slice(0, -1) : result.stdout;
96
+ if (output.length === 0 || output.includes("\n") || output.includes("\r") || output.trim() !== output) return null;
97
+ return isAllowed(output) && isExpectedVaultExit(command, output, result.status) ? output : null;
98
+ }
99
+
100
+ function vaultProbeEnvironment(): NodeJS.ProcessEnv {
101
+ const environment: NodeJS.ProcessEnv = { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" };
102
+ for (const name of ["HOME", "XDG_CONFIG_HOME", "XDG_RUNTIME_DIR", "AIDEVOPS_VAULT_DIR", "AIDEVOPS_VAULT_RUNTIME_DIR", "AIDEVOPS_VAULT_PYTHON"]) {
103
+ const value = process.env[name];
104
+ if (value !== undefined) environment[name] = value;
72
105
  }
106
+ return environment;
107
+ }
108
+
109
+ function coherentVaultState(status: GuiVaultStatus | null, setupState: GuiVaultSetupState | null): boolean {
110
+ if (status === null || setupState === null) return false;
111
+ return COHERENT_VAULT_STATES[status].has(setupState);
112
+ }
113
+
114
+ function isExpectedVaultExit(command: VaultCommand, value: VaultProbeValue, exitCode: number): boolean {
115
+ return EXPECTED_VAULT_EXIT_CODES[command][value] === exitCode;
73
116
  }
74
117
 
75
118
  function isGuiVaultStatus(value: string): value is GuiVaultStatus {
@@ -88,7 +131,7 @@ function vaultHelperStatus(
88
131
  if (!helperExists) {
89
132
  return "missing";
90
133
  }
91
- return status === null && setupState === null ? "error" : "available";
134
+ return status === null || setupState === null ? "error" : "available";
92
135
  }
93
136
 
94
137
  function fallbackSetupState(status: GuiVaultStatus): GuiVaultSetupState {
@@ -198,6 +198,21 @@ notify() {
198
198
  return 0
199
199
  }
200
200
 
201
+ run_vault_command() {
202
+ local action="\$1"
203
+ local command_name=""
204
+ case "\$action" in
205
+ init|unlock|lock|status|lost-passphrase) command_name="\$action" ;;
206
+ *) printf 'Unsupported Vault action\n' >&2; return 2 ;;
207
+ esac
208
+ AIDEVOPS_SESSION_ID="gui-desktop" exec /bin/bash "\${REPO_ROOT}/aidevops.sh" vault "\$command_name"
209
+ return 1
210
+ }
211
+
212
+ if [[ "\$MODE" == "vault-command" ]]; then
213
+ run_vault_command "\${2:-}"
214
+ fi
215
+
201
216
  url_ready() {
202
217
  local url="\$1"
203
218
  curl --silent --fail --max-time 2 "\${url}" >/dev/null 2>&1
@@ -497,6 +512,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
497
512
  updateAccentHueFromMessage(message.body)
498
513
  case "externalLink":
499
514
  openExternalLinkFromMessage(message.body)
515
+ case "vaultCommand":
516
+ openVaultCommandFromMessage(message)
500
517
  default:
501
518
  return
502
519
  }
@@ -525,6 +542,68 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
525
542
  _ = openExternalURL(url)
526
543
  }
527
544
 
545
+ private func openVaultCommandFromMessage(_ message: WKScriptMessage) {
546
+ guard isTrustedVaultMessage(message), let action = message.body as? String else {
547
+ return
548
+ }
549
+
550
+ // #aidevops:trust-boundary — the main local frame selects only a fixed
551
+ // action. The bundled launcher independently allowlists it and resolves
552
+ // the repository CLI without PATH lookup.
553
+ switch action {
554
+ case "init", "unlock", "lock", "status", "lost-passphrase": break
555
+ default: return
556
+ }
557
+
558
+ guard let helperPath = Bundle.main.path(forResource: "aidevops-gui-services", ofType: "sh") else {
559
+ NSSound.beep()
560
+ notifyVaultCommandResult("failed")
561
+ return
562
+ }
563
+ guard helperPath.rangeOfCharacter(from: .newlines) == nil else {
564
+ NSSound.beep()
565
+ notifyVaultCommandResult("failed")
566
+ return
567
+ }
568
+
569
+ let command = "/bin/bash \(shellQuote(helperPath)) vault-command \(shellQuote(action))"
570
+ let source = "tell application \"Terminal\"\nactivate\ndo script \(appleScriptQuote(command))\nend tell"
571
+ guard let script = NSAppleScript(source: source) else {
572
+ NSSound.beep()
573
+ notifyVaultCommandResult("failed")
574
+ return
575
+ }
576
+ var error: NSDictionary?
577
+ script.executeAndReturnError(&error)
578
+ if error != nil {
579
+ NSSound.beep()
580
+ notifyVaultCommandResult("failed")
581
+ return
582
+ }
583
+ notifyVaultCommandResult("opened")
584
+ }
585
+
586
+ private func isTrustedVaultMessage(_ message: WKScriptMessage) -> Bool {
587
+ let origin = message.frameInfo.securityOrigin
588
+ return message.frameInfo.isMainFrame && origin.protocol == "http" && origin.host == "127.0.0.1" && origin.port == Int(webPort)
589
+ }
590
+
591
+ private func shellQuote(_ value: String) -> String {
592
+ return "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
593
+ }
594
+
595
+ private func appleScriptQuote(_ value: String) -> String {
596
+ let escaped = value
597
+ .replacingOccurrences(of: "\\", with: "\\\\")
598
+ .replacingOccurrences(of: "\"", with: "\\\"")
599
+ return "\"\(escaped)\""
600
+ }
601
+
602
+ private func notifyVaultCommandResult(_ result: String) {
603
+ let script = "window.dispatchEvent(new CustomEvent('aidevops:vault-command-result', { detail: '\(result)' }))"
604
+ webView.evaluateJavaScript(script, completionHandler: nil)
605
+ }
606
+
528
607
  private func revealFileURL(_ url: URL) -> Bool {
529
608
  guard url.isFileURL else {
530
609
  return false
@@ -536,7 +615,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
536
615
 
537
616
  func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
538
617
  guard let url = navigationAction.request.url else {
539
- decisionHandler(.allow)
618
+ decisionHandler(.cancel)
540
619
  return
541
620
  }
542
621
 
@@ -550,7 +629,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
550
629
  return
551
630
  }
552
631
 
553
- decisionHandler(.allow)
632
+ if url.absoluteString == "about:blank" && !hasLoadedDashboard {
633
+ decisionHandler(.allow)
634
+ return
635
+ }
636
+
637
+ decisionHandler(.cancel)
554
638
  }
555
639
 
556
640
  func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
@@ -575,7 +659,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
575
659
 
576
660
  private func isLocalAppURL(_ url: URL) -> Bool {
577
661
  let host = url.host ?? ""
578
- return (host == "127.0.0.1" || host == "localhost") && url.port == Int(webPort)
662
+ return url.scheme == "http" && host == "127.0.0.1" && url.port == Int(webPort)
579
663
  }
580
664
 
581
665
  private func configureMenu() {
@@ -838,6 +922,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
838
922
  let userContentController = WKUserContentController()
839
923
  userContentController.add(self, name: "accentHue")
840
924
  userContentController.add(self, name: "externalLink")
925
+ userContentController.add(self, name: "vaultCommand")
841
926
  configuration.userContentController = userContentController
842
927
  webView = WKWebView(frame: .zero, configuration: configuration)
843
928
  webView.navigationDelegate = self
@@ -494,12 +494,7 @@ export const statusFixture: GuiStatusData = {
494
494
  doc_ref: "docs/gui/helper-api-contract.md:101",
495
495
  },
496
496
  ],
497
- secrets: [
498
- {
499
- name: "GITHUB_TOKEN",
500
- status: "unchecked",
501
- },
502
- ],
497
+ secrets: [],
503
498
  placeholders: [
504
499
  "Settings, repos, routines, OpenCode sessions, and capabilities will be added as read-only adapters.",
505
500
  ],
@@ -1,15 +1,15 @@
1
- import type { GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
2
- import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactElement, useEffect, useRef, useState } from "react";
1
+ import { type ReactElement, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, useEffect, useState } from "react";
3
2
  import { MachineRail, Sidebar } from "./AppNavigation";
4
3
  import { Workspace } from "./AppWorkspace";
5
- import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
4
+ import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, ThemePreference } from "./app-model";
6
5
  import { DEFAULT_ACCENT_HUE, DEFAULT_CONTRAST, DEFAULT_FONT, DEFAULT_FONT_SIZE, fileRootBySurface, findSurface, findSurfaceSectionLabel, fontFamilyForPreference, fontSizeForPreference, getSystemTheme, isContrastPreference, isFontPreference, isFontSizePreference } from "./app-model";
7
6
  import { DesktopStatusBar } from "./DesktopStatusBar";
8
- import { type NavigationHistory, nextHistoryIndex, useNavigationHistoryKeyboard } from "./navigation-history";
9
7
  import { ScreenshotCaptureNotificationHost } from "./ScreenshotCaptureNotification";
10
- import { fetchStatus, mockedStatus } from "./status-client";
11
- import type { VaultDialogIntent } from "./VaultBadges";
12
- import { VaultPassphraseModal } from "./VaultPassphraseModal";
8
+ import { useAppNavigation } from "./useAppNavigation";
9
+ import { useGuiStatus } from "./useGuiStatus";
10
+ import { VaultAccessModal } from "./VaultAccessModal";
11
+
12
+ export { shouldPromptVaultSetup } from "./useGuiStatus";
13
13
 
14
14
  interface WebKitBridgeWindow extends Window {
15
15
  webkit?: {
@@ -92,9 +92,6 @@ export function readStoredAppearancePreferences(storage: ReadableAppearanceStora
92
92
 
93
93
  export function App(): ReactElement {
94
94
  const [storedAppearancePreferences] = useState<StoredAppearancePreferences>(() => readStoredAppearancePreferences());
95
- const [status, setStatus] = useState<GuiResponseEnvelope<GuiStatusData>>(mockedStatus());
96
- const [statusLoading, setStatusLoading] = useState(true);
97
- const [navigation, setNavigation] = useState<NavigationHistory>({ entries: ["overview"], index: 0 });
98
95
  const [themePreference, setThemePreference] = useState<ThemePreference>(storedAppearancePreferences.themePreference);
99
96
  const [accentHue, setAccentHue] = useState(storedAppearancePreferences.accentHue);
100
97
  const [contrastPreference, setContrastPreference] = useState<ContrastPreference>(storedAppearancePreferences.contrastPreference);
@@ -107,38 +104,13 @@ export function App(): ReactElement {
107
104
  const [conversationMode, setConversationMode] = useState<ConversationMode>("ai");
108
105
  const [selectedLocalRepoIndex, setSelectedLocalRepoIndex] = useState(0);
109
106
  const [selectedSessionId, setSelectedSessionId] = useState<string | undefined>();
110
- const [vaultDialogIntent, setVaultDialogIntent] = useState<VaultDialogIntent | null>(null);
111
- const hasPromptedVaultSetup = useRef(false);
112
107
  const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
108
+ const { activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface } = useAppNavigation();
109
+ const { markVaultTerminalLaunch, refreshStatus, setVaultDialogIntent, status, statusLoading, vaultDialogIntent } = useGuiStatus();
113
110
  const resolvedTheme = themePreference === "system" ? systemTheme : themePreference;
114
- const activeSurface: SurfaceId = navigation.entries[navigation.index] ?? "overview";
115
111
  const activeItem = findSurface(activeSurface);
116
112
  const activeSectionLabel = findSurfaceSectionLabel(activeSurface);
117
- const canGoBack = navigation.index > 0;
118
- const canGoForward = navigation.index < navigation.entries.length - 1;
119
113
  const fileRoot = fileRootBySurface[activeSurface];
120
- useNavigationHistoryKeyboard(setNavigation);
121
-
122
- const setActiveSurface = (surface: SurfaceId) => {
123
- setNavigation((current) => {
124
- const currentSurface = current.entries[current.index] ?? "overview";
125
- if (currentSurface === surface) {
126
- return current;
127
- }
128
-
129
- const entries = current.entries.slice(0, current.index + 1);
130
- entries.push(surface);
131
- return { entries, index: entries.length - 1 };
132
- });
133
- };
134
-
135
- const goBack = () => {
136
- setNavigation((current) => ({ ...current, index: nextHistoryIndex(current, -1) }));
137
- };
138
-
139
- const goForward = () => {
140
- setNavigation((current) => ({ ...current, index: nextHistoryIndex(current, 1) }));
141
- };
142
114
 
143
115
  useEffect(() => {
144
116
  const isMacosDesktop = isMacosDesktopBrowser();
@@ -189,13 +161,6 @@ export function App(): ReactElement {
189
161
  persistAppearancePreference(appearanceStorageKeys.fontSize, fontSizePreference);
190
162
  }, [fontSizePreference]);
191
163
 
192
- useEffect(() => {
193
- fetchStatus()
194
- .then(setStatus)
195
- .catch(() => setStatus(mockedStatus()))
196
- .finally(() => setStatusLoading(false));
197
- }, []);
198
-
199
164
  useEffect(() => {
200
165
  persistAppearancePreference(appearanceStorageKeys.showNavCounts, String(showNavCounts));
201
166
  }, [showNavCounts]);
@@ -213,13 +178,6 @@ export function App(): ReactElement {
213
178
  setSelectedLocalRepoIndex((current) => Math.min(current, Math.max(0, status.data.local_repos.repos.length - 1)));
214
179
  }, [status.data.local_repos.repos.length]);
215
180
 
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
-
223
181
  return (
224
182
  <main className={machineRailVisible ? "app-shell" : "app-shell machine-rail-collapsed"} aria-busy={statusLoading}>
225
183
  <div className="desktop-titlebar-tagline" aria-hidden="true">Your data protected. Your systems managed. Your creations published.</div>
@@ -276,7 +234,7 @@ export function App(): ReactElement {
276
234
  status={status.data}
277
235
  />
278
236
  <DesktopStatusBar status={status.data} />
279
- {vaultDialogIntent ? <VaultPassphraseModal intent={vaultDialogIntent} onClose={() => setVaultDialogIntent(null)} vault={status.data.vault} /> : null}
237
+ {vaultDialogIntent ? <VaultAccessModal intent={vaultDialogIntent} onClose={() => setVaultDialogIntent(null)} onRefresh={refreshStatus} onTerminalLaunch={markVaultTerminalLaunch} vault={status.data.vault} /> : null}
280
238
  </main>
281
239
  );
282
240
  }
@@ -345,10 +303,6 @@ export function clampSidebarWidth(width: number): number {
345
303
  return Math.min(maxSidebarWidth, Math.max(minSidebarWidth, Math.round(width)));
346
304
  }
347
305
 
348
- export function shouldPromptVaultSetup(statusLoading: boolean, setupRequired: boolean, hasPromptedVaultSetup: boolean): boolean {
349
- return !statusLoading && setupRequired && !hasPromptedVaultSetup;
350
- }
351
-
352
306
  function SidebarResizeHandle(): ReactElement {
353
307
  const [sidebarWidth, setSidebarWidth] = useState(() => readStoredSidebarWidth());
354
308
 
@@ -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 { type VaultDialogIntent, isVaultSurfaceLocked, vaultCollectionForSurface, vaultDialogIntentForStatus } from "./VaultBadges";
15
+ import { type VaultDialogIntent, isVaultSurfaceLocked, vaultActionLabel, vaultCollectionForSurface, vaultDialogIntentForStatus } from "./VaultBadges";
16
16
  import { applyCommandPaletteSelection, useHeaderMenuState } from "./workspace-header-state";
17
17
  import { useWorkspaceTour, WorkspaceTourProvider } from "./WorkspaceTour";
18
18
 
@@ -370,7 +370,7 @@ function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistan
370
370
 
371
371
  function ProfileMenu({ onVaultRequest, openSurface, status, userName }: { onVaultRequest: (intent: VaultDialogIntent) => void; openSurface: (surface: SurfaceId) => void; status: GuiStatusData; userName: string }): ReactElement {
372
372
  const vaultIntent = vaultDialogIntentForStatus(status.vault);
373
- const VaultIcon = vaultIntent === "lock" ? FiLock : FiUnlock;
373
+ const VaultIcon = vaultIntent === "lock" ? FiLock : vaultIntent === "recover" || vaultIntent === "unavailable" ? FiAlertTriangle : FiUnlock;
374
374
  return (
375
375
  <div className="popover-menu profile-menu" role="menu">
376
376
  <div className="profile-menu-heading">
@@ -380,7 +380,7 @@ function ProfileMenu({ onVaultRequest, openSurface, status, userName }: { onVaul
380
380
  <button onClick={() => openSurface("settings")} role="menuitem" type="button"><FiSettings aria-hidden="true" /> Settings</button>
381
381
  <button onClick={() => openSurface("settings")} role="menuitem" type="button"><FiCommand aria-hidden="true" /> Theme</button>
382
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>
383
+ <button onClick={() => onVaultRequest(vaultIntent)} role="menuitem" type="button"><VaultIcon aria-hidden="true" /> {vaultActionLabel(vaultIntent)}</button>
384
384
  <div className="menu-separator" />
385
385
  <strong>Community</strong>
386
386
  <a href={communityLinks.github} rel="noreferrer" role="menuitem" target="_blank"><FiMessageSquare aria-hidden="true" /> GitHub</a>
@@ -453,11 +453,11 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, onVaultRequest, o
453
453
  apps: <AppsSurface status={status} />,
454
454
  installation: <InstallationSurface />,
455
455
  projects: <ProjectsSurface status={status} />,
456
- security: <SecuritySurface status={status} />,
456
+ security: <SecuritySurface onVaultRequest={onVaultRequest} status={status} />,
457
457
  aiProviders: <AiProvidersSurface status={status} />,
458
458
  };
459
459
 
460
- if (isVaultSurfaceLocked(status.vault, activeSurface) && vaultCollection) {
460
+ if (activeSurface !== "security" && isVaultSurfaceLocked(status.vault, activeSurface) && vaultCollection) {
461
461
  return <LockedVaultGate collection={vaultCollection} label={activeItem.label} onVaultRequest={onVaultRequest} vault={status.vault} />;
462
462
  }
463
463
 
@@ -19,7 +19,7 @@ export function DesktopStatusBar({ status }: { status: GuiStatusData }): ReactEl
19
19
  <span>{versionLabel}</span>
20
20
  <span>{localRepoCount} local repos</span>
21
21
  <span>{remoteRepoCount} remote repos</span>
22
- <span>{status.secrets.length} secrets</span>
22
+ <span>{status.vault.unlocked ? `${status.secrets.length} secret references` : "Secret references hidden"}</span>
23
23
  <span>{vaultLabel}</span>
24
24
  <span>{oauthAccounts} provider accounts</span>
25
25
  <span>{needsUpdate} need update</span>