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 +10 -10
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/status-adapter.ts +1 -0
- package/packages/gui-api/src/status-vault.ts +61 -18
- package/packages/gui-desktop/scripts/install-macos-app.sh +88 -3
- package/packages/gui-shared/src/fixtures.ts +1 -6
- package/packages/gui-web/src/App.tsx +10 -56
- package/packages/gui-web/src/AppWorkspace.tsx +5 -5
- package/packages/gui-web/src/DesktopStatusBar.tsx +1 -1
- package/packages/gui-web/src/SecuritySurface.tsx +187 -0
- package/packages/gui-web/src/StatusSurfaces.tsx +4 -170
- package/packages/gui-web/src/VaultAccessModal.tsx +123 -0
- package/packages/gui-web/src/VaultBadges.tsx +96 -36
- package/packages/gui-web/src/VaultSurface.tsx +282 -0
- package/packages/gui-web/src/status-client.ts +72 -24
- package/packages/gui-web/src/styles.css +385 -34
- package/packages/gui-web/src/useAppNavigation.ts +55 -0
- package/packages/gui-web/src/useGuiStatus.ts +79 -0
- package/packages/gui-web/src/useVaultAccessDialog.ts +160 -0
- package/packages/gui-web/src/vault-command-bridge.ts +27 -0
- package/setup.sh +3 -1
- package/packages/gui-web/src/VaultPassphraseModal.tsx +0 -147
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)
|
|
@@ -116,9 +116,9 @@ the required notices and preferred credit text.
|
|
|
116
116
|
|
|
117
117
|
### Agent Structure
|
|
118
118
|
|
|
119
|
-
-
|
|
120
|
-
- 2,
|
|
121
|
-
- 1,
|
|
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,
|
|
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,
|
|
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:
|
|
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,
|
|
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,
|
|
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.
|
|
1
|
+
3.32.6
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
27
|
-
const
|
|
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:
|
|
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:
|
|
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
|
|
83
|
+
function readVaultCommand<T extends VaultProbeValue>(
|
|
58
84
|
helperPath: string,
|
|
59
|
-
|
|
85
|
+
command: VaultCommand,
|
|
60
86
|
isAllowed: (value: string) => value is T,
|
|
61
87
|
): T | null {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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(.
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
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 ? <
|
|
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
|
|
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}
|
|
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>
|