aidevops 3.29.5 → 3.29.7
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-api/src/status-adapter.ts +13 -1
- package/packages/gui-shared/src/contracts.ts +26 -0
- package/packages/gui-shared/src/fixtures.ts +13 -0
- package/packages/gui-shared/src/index.ts +1 -0
- package/packages/gui-shared/src/notifications.ts +220 -0
- package/packages/gui-web/src/AppWorkspace.tsx +87 -19
- package/packages/gui-web/src/InventorySurfaces.tsx +74 -21
- package/packages/gui-web/src/app-model.ts +27 -30
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +390 -27
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.29.
|
|
1
|
+
3.29.7
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type GuiSetupTargetSummary,
|
|
21
21
|
type GuiStatusData,
|
|
22
22
|
type GuiVaultStatusData,
|
|
23
|
+
buildStatusNotifications,
|
|
23
24
|
statusFixture,
|
|
24
25
|
VAULT_STATUS_ROUTE_MANIFEST,
|
|
25
26
|
} from "../../gui-shared/src";
|
|
@@ -60,6 +61,7 @@ export function readStatus(
|
|
|
60
61
|
const reposPathRef = "~/.config/aidevops/repos.json";
|
|
61
62
|
const oauthPoolPathRef = "~/.aidevops/oauth-pool.json";
|
|
62
63
|
const opencodeDbPathRef = "~/.local/share/opencode/opencode.db";
|
|
64
|
+
const greetingCachePathRef = "~/.aidevops/cache/session-greeting.txt";
|
|
63
65
|
const agentsPathRef = "~/.aidevops/agents";
|
|
64
66
|
const vaultPathRef = statusFixture.vault.path_ref;
|
|
65
67
|
const settingsPath = expandHome(settingsPathRef);
|
|
@@ -76,13 +78,22 @@ export function readStatus(
|
|
|
76
78
|
const managedApps = readManagedApps(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
77
79
|
const localRepos = readLocalReposSetupSummary(reposPath);
|
|
78
80
|
const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
|
|
81
|
+
const oauthPool = readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef);
|
|
79
82
|
const vault = readVaultSummary(repoRoot);
|
|
83
|
+
const notifications = buildStatusNotifications({
|
|
84
|
+
aiApps,
|
|
85
|
+
greetingOutput: readOptionalText(expandHome(greetingCachePathRef)) ?? "",
|
|
86
|
+
oauthPool,
|
|
87
|
+
restartRequired,
|
|
88
|
+
setupTargets,
|
|
89
|
+
});
|
|
80
90
|
const sourcePathRefs = [
|
|
81
91
|
agentsPathRef,
|
|
82
92
|
settingsPathRef,
|
|
83
93
|
reposPathRef,
|
|
84
94
|
oauthPoolPathRef,
|
|
85
95
|
opencodeDbPathRef,
|
|
96
|
+
greetingCachePathRef,
|
|
86
97
|
vaultPathRef,
|
|
87
98
|
"VERSION",
|
|
88
99
|
...setupTargets.map((target) => target.path_ref),
|
|
@@ -131,10 +142,11 @@ export function readStatus(
|
|
|
131
142
|
repos: readRepoRegistrySummary(reposPath, reposPathRef),
|
|
132
143
|
local_repos: localRepos,
|
|
133
144
|
opencode_sessions: opencodeSessions,
|
|
134
|
-
oauth_pool:
|
|
145
|
+
oauth_pool: oauthPool,
|
|
135
146
|
setup_targets: setupTargets,
|
|
136
147
|
ai_apps: aiApps,
|
|
137
148
|
managed_apps: managedApps,
|
|
149
|
+
notifications,
|
|
138
150
|
vault,
|
|
139
151
|
};
|
|
140
152
|
|
|
@@ -339,6 +339,31 @@ export interface GuiCapabilitySummary {
|
|
|
339
339
|
doc_ref: string;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
export type GuiNotificationSeverity = "success" | "info" | "warning" | "error";
|
|
343
|
+
|
|
344
|
+
export type GuiNotificationCategory = "security" | "maintenance" | "release" | "runtime" | "setup";
|
|
345
|
+
|
|
346
|
+
export interface GuiNotificationAction {
|
|
347
|
+
id: string;
|
|
348
|
+
label: string;
|
|
349
|
+
kind: "surface" | "command";
|
|
350
|
+
surface_id?: string;
|
|
351
|
+
command_preview?: string;
|
|
352
|
+
enabled: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface GuiNotificationSummary {
|
|
356
|
+
id: string;
|
|
357
|
+
title: string;
|
|
358
|
+
message: string;
|
|
359
|
+
severity: GuiNotificationSeverity;
|
|
360
|
+
category: GuiNotificationCategory;
|
|
361
|
+
source: "opencode-toast" | "gui-status";
|
|
362
|
+
source_ref: string;
|
|
363
|
+
status: "active" | "resolved";
|
|
364
|
+
actions: GuiNotificationAction[];
|
|
365
|
+
}
|
|
366
|
+
|
|
342
367
|
export interface GuiMachineSummary {
|
|
343
368
|
id: string;
|
|
344
369
|
label: string;
|
|
@@ -451,6 +476,7 @@ export interface GuiStatusData {
|
|
|
451
476
|
setup_targets: GuiSetupTargetSummary[];
|
|
452
477
|
ai_apps: GuiAiAppSummary[];
|
|
453
478
|
managed_apps: GuiManagedAppSummary[];
|
|
479
|
+
notifications: GuiNotificationSummary[];
|
|
454
480
|
vault: GuiVaultStatusData;
|
|
455
481
|
capabilities: GuiCapabilitySummary[];
|
|
456
482
|
secrets: GuiSecretReference[];
|
|
@@ -163,6 +163,19 @@ export const statusFixture: GuiStatusData = {
|
|
|
163
163
|
},
|
|
164
164
|
],
|
|
165
165
|
managed_apps: [],
|
|
166
|
+
notifications: [
|
|
167
|
+
{
|
|
168
|
+
id: "fixture-security-active",
|
|
169
|
+
title: "Security protections active",
|
|
170
|
+
message: "Security: all protections active",
|
|
171
|
+
severity: "success",
|
|
172
|
+
category: "security",
|
|
173
|
+
source: "opencode-toast",
|
|
174
|
+
source_ref: "~/.aidevops/cache/session-greeting.txt",
|
|
175
|
+
status: "resolved",
|
|
176
|
+
actions: [{ id: "open-security", label: "Open security", kind: "surface", surface_id: "security", enabled: true }],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
166
179
|
vault: {
|
|
167
180
|
status: "uninitialized",
|
|
168
181
|
setup_state: "uninitialized",
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { GuiAiAppSummary, GuiNotificationAction, GuiNotificationCategory, GuiNotificationSeverity, GuiNotificationSummary, GuiOAuthPoolSummary, GuiSetupTargetSummary } from "./contracts";
|
|
2
|
+
|
|
3
|
+
interface BuildNotificationInput {
|
|
4
|
+
aiApps: GuiAiAppSummary[];
|
|
5
|
+
greetingOutput: string;
|
|
6
|
+
oauthPool: GuiOAuthPoolSummary;
|
|
7
|
+
restartRequired: boolean;
|
|
8
|
+
setupTargets: GuiSetupTargetSummary[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const toastSeverityRank: Record<GuiNotificationSeverity, number> = {
|
|
12
|
+
success: 0,
|
|
13
|
+
info: 1,
|
|
14
|
+
warning: 2,
|
|
15
|
+
error: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const warningLineMatchers: Array<(line: string) => boolean> = [
|
|
19
|
+
(line) => line.startsWith("Pulse stalled"),
|
|
20
|
+
(line) => /contribution\(s\) need/i.test(line),
|
|
21
|
+
(line) => line.startsWith("[OPENCODE MAINTENANCE]"),
|
|
22
|
+
(line) => line.startsWith("[WARNING]"),
|
|
23
|
+
(line) => line.startsWith("[WARN]"),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const toastTitleRules: Array<{ matches: (line: string) => boolean; title: string }> = [
|
|
27
|
+
{ matches: (line) => line.startsWith("[SECURITY ADVISORY]"), title: "Security advisory" },
|
|
28
|
+
{ matches: (line) => line.startsWith("[ERROR]"), title: "Startup error" },
|
|
29
|
+
{ matches: (line) => line.startsWith("[OPENCODE MAINTENANCE]"), title: "OpenCode maintenance" },
|
|
30
|
+
{ matches: (line) => line.startsWith("Pulse stalled"), title: "Pulse stalled" },
|
|
31
|
+
{ matches: (line) => /contribution\(s\) need/i.test(line), title: "External contribution review" },
|
|
32
|
+
{ matches: (line) => line.startsWith("[WARNING]") || line.startsWith("[WARN]"), title: "Startup warning" },
|
|
33
|
+
{ matches: (line) => line.startsWith("Security: all protections active"), title: "Security protections active" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function classifyToastLine(line: string): GuiNotificationSeverity | null {
|
|
37
|
+
const normalized = line.trim();
|
|
38
|
+
if (normalized.length === 0 || normalized.startsWith("UPDATE_AVAILABLE|") || normalized === "AUTO_UPDATE_ENABLED") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (normalized.startsWith("[SECURITY ADVISORY]") || normalized.startsWith("[ERROR]")) {
|
|
43
|
+
return "error";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (warningLineMatchers.some((matches) => matches(normalized))) {
|
|
47
|
+
return "warning";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (normalized.startsWith("Security: all protections active")) {
|
|
51
|
+
return "success";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "info";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildStatusNotifications(input: BuildNotificationInput): GuiNotificationSummary[] {
|
|
58
|
+
const notifications = [
|
|
59
|
+
...buildToastNotifications(input.greetingOutput),
|
|
60
|
+
...buildGuiStateNotifications(input),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return dedupeNotifications(notifications).sort((left, right) => {
|
|
64
|
+
const severityDelta = toastSeverityRank[right.severity] - toastSeverityRank[left.severity];
|
|
65
|
+
return severityDelta === 0 ? left.title.localeCompare(right.title) : severityDelta;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildToastNotifications(greetingOutput: string): GuiNotificationSummary[] {
|
|
70
|
+
return greetingOutput
|
|
71
|
+
.split("\n")
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.flatMap((line) => {
|
|
75
|
+
const severity = classifyToastLine(line);
|
|
76
|
+
if (severity === null) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const category = categoryForToastLine(line, severity);
|
|
81
|
+
return [notification({
|
|
82
|
+
id: `toast-${stableId(line)}`,
|
|
83
|
+
title: titleForToastLine(line, severity),
|
|
84
|
+
message: line,
|
|
85
|
+
severity,
|
|
86
|
+
category,
|
|
87
|
+
source: "opencode-toast",
|
|
88
|
+
source_ref: "~/.aidevops/cache/session-greeting.txt",
|
|
89
|
+
status: severity === "success" || severity === "info" ? "resolved" : "active",
|
|
90
|
+
actions: actionsForToastLine(line, category, severity),
|
|
91
|
+
})];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificationSummary[] {
|
|
96
|
+
const setupNeedsUpdate = input.setupTargets.filter((target) => target.needs_update);
|
|
97
|
+
const appNeedsUpdate = input.aiApps.filter((app) => app.needs_update);
|
|
98
|
+
const authErrors = input.oauthPool.providers.filter((provider) => provider.auth_errors > 0);
|
|
99
|
+
const rateLimited = input.oauthPool.providers.filter((provider) => provider.rate_limited > 0);
|
|
100
|
+
const notifications: GuiNotificationSummary[] = [];
|
|
101
|
+
|
|
102
|
+
if (input.restartRequired) {
|
|
103
|
+
notifications.push(notification({
|
|
104
|
+
id: "gui-restart-required",
|
|
105
|
+
title: "Restart required",
|
|
106
|
+
message: "aidevops has updated in the background. Restart the GUI app to use the latest installed version.",
|
|
107
|
+
severity: "warning",
|
|
108
|
+
category: "release",
|
|
109
|
+
source: "gui-status",
|
|
110
|
+
source_ref: "VERSION and ~/.aidevops/agents/VERSION",
|
|
111
|
+
status: "active",
|
|
112
|
+
actions: [surfaceAction("open-installation", "Open installation", "installation"), commandAction("restart-gui", "Restart GUI app", "Quit and reopen the aidevops GUI app")],
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (setupNeedsUpdate.length > 0 || appNeedsUpdate.length > 0) {
|
|
117
|
+
notifications.push(notification({
|
|
118
|
+
id: "gui-targets-need-update",
|
|
119
|
+
title: "Installed targets need update",
|
|
120
|
+
message: `${setupNeedsUpdate.length} setup target(s) and ${appNeedsUpdate.length} AI app target(s) need the current aidevops files.`,
|
|
121
|
+
severity: "warning",
|
|
122
|
+
category: "setup",
|
|
123
|
+
source: "gui-status",
|
|
124
|
+
source_ref: "setup_targets and ai_apps",
|
|
125
|
+
status: "active",
|
|
126
|
+
actions: [surfaceAction("open-installation", "Open installation", "installation"), commandAction("run-update", "Run update", "aidevops update")],
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const provider of authErrors) {
|
|
131
|
+
notifications.push(notification({
|
|
132
|
+
id: `gui-provider-auth-${provider.provider}`,
|
|
133
|
+
title: `${provider.provider} auth attention`,
|
|
134
|
+
message: `${provider.auth_errors} ${provider.provider} account(s) report auth errors. Token values remain hidden from the GUI payload.`,
|
|
135
|
+
severity: "error",
|
|
136
|
+
category: "runtime",
|
|
137
|
+
source: "gui-status",
|
|
138
|
+
source_ref: input.oauthPool.path_ref,
|
|
139
|
+
status: "active",
|
|
140
|
+
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders"), commandAction("check-oauth-pool", "Check provider pool", `aidevops oauth-pool check ${provider.provider}`)],
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const provider of rateLimited) {
|
|
145
|
+
notifications.push(notification({
|
|
146
|
+
id: `gui-provider-rate-limit-${provider.provider}`,
|
|
147
|
+
title: `${provider.provider} rate limit`,
|
|
148
|
+
message: `${provider.rate_limited} ${provider.provider} account(s) are cooling down. The pool should recover automatically when cooldowns expire.`,
|
|
149
|
+
severity: "warning",
|
|
150
|
+
category: "runtime",
|
|
151
|
+
source: "gui-status",
|
|
152
|
+
source_ref: input.oauthPool.path_ref,
|
|
153
|
+
status: "active",
|
|
154
|
+
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders")],
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return notifications;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function notification(input: GuiNotificationSummary): GuiNotificationSummary {
|
|
162
|
+
return input;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function titleForToastLine(line: string, severity: GuiNotificationSeverity): string {
|
|
166
|
+
const rule = toastTitleRules.find((candidate) => candidate.matches(line));
|
|
167
|
+
return rule?.title ?? (severity === "info" ? "Runtime status" : "aidevops status");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function categoryForToastLine(line: string, severity: GuiNotificationSeverity): GuiNotificationCategory {
|
|
171
|
+
if (line.startsWith("[SECURITY ADVISORY]") || line.startsWith("Security:")) return "security";
|
|
172
|
+
if (line.startsWith("[OPENCODE MAINTENANCE]") || line.startsWith("Pulse stalled")) return "maintenance";
|
|
173
|
+
if (/update|version|running in/i.test(line)) return "release";
|
|
174
|
+
if (severity === "warning") return "setup";
|
|
175
|
+
return "runtime";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function actionsForToastLine(line: string, category: GuiNotificationCategory, severity: GuiNotificationSeverity): GuiNotificationAction[] {
|
|
179
|
+
if (category === "security") {
|
|
180
|
+
return [surfaceAction("open-security", "Open security", "security"), surfaceAction("open-vault", "Open Vault", "vault")];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (line.startsWith("[OPENCODE MAINTENANCE]")) {
|
|
184
|
+
return [surfaceAction("open-local-setup", "Open local setup", "localSetup"), commandAction("run-maintenance", "Run maintenance", "opencode-db-maintenance-helper.sh run")];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (line.startsWith("Pulse stalled")) {
|
|
188
|
+
return [surfaceAction("open-maintenance", "Open maintenance", "maintenance")];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (severity === "warning") {
|
|
192
|
+
return [surfaceAction("open-installation", "Open installation", "installation"), commandAction("run-update", "Run update", "aidevops update")];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return [surfaceAction("open-overview", "Open overview", "overview")];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function surfaceAction(id: string, label: string, surfaceId: string): GuiNotificationAction {
|
|
199
|
+
return { id, label, kind: "surface", surface_id: surfaceId, enabled: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function commandAction(id: string, label: string, commandPreview: string): GuiNotificationAction {
|
|
203
|
+
return { id, label, kind: "command", command_preview: commandPreview, enabled: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stableId(value: string): string {
|
|
207
|
+
let hash = 0;
|
|
208
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
209
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0;
|
|
210
|
+
}
|
|
211
|
+
return Math.abs(hash).toString(36);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function dedupeNotifications(notifications: GuiNotificationSummary[]): GuiNotificationSummary[] {
|
|
215
|
+
const byId = new Map<string, GuiNotificationSummary>();
|
|
216
|
+
for (const item of notifications) {
|
|
217
|
+
byId.set(item.id, item);
|
|
218
|
+
}
|
|
219
|
+
return [...byId.values()];
|
|
220
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { type ReactElement, type ReactNode, useEffect, useState } from "react";
|
|
3
|
-
import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiUser } from "react-icons/fi";
|
|
4
|
-
import type { GuiFileRootId, GuiStatusData } from "../../gui-shared/src";
|
|
3
|
+
import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiInfo, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUser } from "react-icons/fi";
|
|
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";
|
|
7
|
-
import { inventorySurfaceConfigs, text } from "./app-model";
|
|
7
|
+
import { inventorySurfaceConfigs, surfaceIds, text } from "./app-model";
|
|
8
8
|
import { CommandPalette, type CommandPaletteSelection, commandPaletteShortcutQuery } from "./CommandPalette";
|
|
9
9
|
import { FileExplorerSurface } from "./FileExplorerSurface";
|
|
10
10
|
import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
|
|
@@ -42,7 +42,7 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
|
|
|
42
42
|
<div className="workspace-scroll">
|
|
43
43
|
{shellMode === "sessions"
|
|
44
44
|
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSessionId} status={status} />
|
|
45
|
-
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />}
|
|
45
|
+
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} openSurface={setActiveSurface} status={status} />}
|
|
46
46
|
</div>
|
|
47
47
|
</section>
|
|
48
48
|
);
|
|
@@ -70,6 +70,7 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
70
70
|
const [profileOpen, setProfileOpen] = useState(false);
|
|
71
71
|
const userInitials = status.machine.initials || "AI";
|
|
72
72
|
const userName = status.machine.username || "Local user";
|
|
73
|
+
const activeNotifications = status.notifications.filter((notification) => notification.status === "active");
|
|
73
74
|
|
|
74
75
|
useEffect(() => {
|
|
75
76
|
const openCommandPalette = (event: KeyboardEvent) => {
|
|
@@ -134,9 +135,9 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
134
135
|
</button>
|
|
135
136
|
<button aria-expanded={notificationsOpen} aria-label="Open notifications" className="header-icon-button" onClick={() => { setNotificationsOpen((current) => !current); setProfileOpen(false); }} type="button">
|
|
136
137
|
<FiBell aria-hidden="true" />
|
|
137
|
-
<span className="notification-dot" aria-hidden="true" />
|
|
138
|
+
{activeNotifications.length > 0 ? <span className="notification-dot" aria-hidden="true" /> : null}
|
|
138
139
|
</button>
|
|
139
|
-
{notificationsOpen ? <NotificationsMenu openSurface={(surface) => openItem({ surface })} /> : null}
|
|
140
|
+
{notificationsOpen ? <NotificationsMenu notifications={status.notifications} openSurface={(surface) => openItem({ surface })} /> : null}
|
|
140
141
|
</div>
|
|
141
142
|
<button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} title="AI sessions (_)" type="button">
|
|
142
143
|
<FiTerminal aria-hidden="true" />
|
|
@@ -176,16 +177,35 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
|
|
|
176
177
|
return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
180
|
+
function NotificationsMenu({ notifications, openSurface }: { notifications: GuiNotificationSummary[]; openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
181
|
+
const active = notifications.filter((notification) => notification.status === "active");
|
|
182
|
+
const preview = notifications.slice(0, 4);
|
|
183
|
+
|
|
180
184
|
return (
|
|
181
185
|
<div className="popover-menu notifications-menu" role="menu">
|
|
182
|
-
<
|
|
183
|
-
|
|
186
|
+
<div className="notifications-menu-heading">
|
|
187
|
+
<strong>Notifications</strong>
|
|
188
|
+
<span>{active.length} active</span>
|
|
189
|
+
</div>
|
|
190
|
+
{preview.length === 0 ? <p>No current aidevops notifications.</p> : null}
|
|
191
|
+
{preview.map((notification) => <NotificationPreview key={notification.id} notification={notification} />)}
|
|
184
192
|
<button onClick={() => openSurface("notifications")} role="menuitem" type="button">Open notifications</button>
|
|
185
193
|
</div>
|
|
186
194
|
);
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
function NotificationPreview({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
198
|
+
return (
|
|
199
|
+
<article className={`notification-preview ${notification.severity}`}>
|
|
200
|
+
<span aria-hidden="true"><NotificationIcon notification={notification} /></span>
|
|
201
|
+
<div>
|
|
202
|
+
<strong>{notification.title}</strong>
|
|
203
|
+
<small>{notification.category} · {notification.status}</small>
|
|
204
|
+
</div>
|
|
205
|
+
</article>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
189
209
|
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selectedSessionId, status }: {
|
|
190
210
|
conversationMode: ConversationMode;
|
|
191
211
|
selectedLocalRepoIndex: number;
|
|
@@ -268,10 +288,11 @@ function AssistantPanel({ activeSurface, userName }: { activeSurface: SurfaceId;
|
|
|
268
288
|
);
|
|
269
289
|
}
|
|
270
290
|
|
|
271
|
-
function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
291
|
+
function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, status }: {
|
|
272
292
|
activeItem: SurfaceNavItem;
|
|
273
293
|
activeSurface: SurfaceId;
|
|
274
294
|
fileRoot: GuiFileRootId | undefined;
|
|
295
|
+
openSurface: (surface: SurfaceId) => void;
|
|
275
296
|
status: GuiStatusData;
|
|
276
297
|
}) {
|
|
277
298
|
const inventoryConfig = inventorySurfaceConfigs[activeSurface];
|
|
@@ -280,7 +301,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
280
301
|
overview: <OverviewSurface status={status} />,
|
|
281
302
|
help: <HelpSurface />,
|
|
282
303
|
settings: <SettingsSurface status={status} />,
|
|
283
|
-
notifications: <NotificationsSurface />,
|
|
304
|
+
notifications: <NotificationsSurface openSurface={openSurface} status={status} />,
|
|
284
305
|
admin: <AdminSurface />,
|
|
285
306
|
vault: <VaultSurface status={status} />,
|
|
286
307
|
aiSessions: <PlannedSurface label={text.aiSessions} detail={text.aiSessionsIntro} />,
|
|
@@ -391,22 +412,69 @@ function SettingsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
|
391
412
|
);
|
|
392
413
|
}
|
|
393
414
|
|
|
394
|
-
function NotificationsSurface(): ReactElement {
|
|
415
|
+
function NotificationsSurface({ openSurface, status }: { openSurface: (surface: SurfaceId) => void; status: GuiStatusData }): ReactElement {
|
|
416
|
+
const active = status.notifications.filter((notification) => notification.status === "active");
|
|
417
|
+
const resolved = status.notifications.filter((notification) => notification.status === "resolved");
|
|
418
|
+
|
|
395
419
|
return (
|
|
396
|
-
<section className="settings-surface">
|
|
397
|
-
<div className="planned-card">
|
|
420
|
+
<section className="settings-surface notifications-surface">
|
|
421
|
+
<div className="planned-card notifications-hero">
|
|
398
422
|
<h2>Notifications</h2>
|
|
399
|
-
<p>
|
|
423
|
+
<p>OpenCode startup toasts and the GUI notification centre read the same aidevops status cache and local readiness data, so resolved items disappear or downgrade everywhere after the underlying status changes.</p>
|
|
424
|
+
<section aria-label="Notification summary" className="notification-summary-strip">
|
|
425
|
+
<span><strong>{active.length}</strong> active</span>
|
|
426
|
+
<span><strong>{resolved.length}</strong> resolved</span>
|
|
427
|
+
<span><strong>{status.notifications.length}</strong> total</span>
|
|
428
|
+
</section>
|
|
400
429
|
</div>
|
|
401
|
-
<ul className="notification-list">
|
|
402
|
-
<
|
|
403
|
-
<
|
|
404
|
-
<li><strong>Security</strong><span>Vault and credentials posture reminders.</span></li>
|
|
430
|
+
<ul aria-label="aidevops notifications" className="github-notification-list">
|
|
431
|
+
{status.notifications.length === 0 ? <p className="empty-state">No aidevops notifications are currently reported.</p> : null}
|
|
432
|
+
{status.notifications.map((notification) => <NotificationCard key={notification.id} notification={notification} openSurface={openSurface} />)}
|
|
405
433
|
</ul>
|
|
406
434
|
</section>
|
|
407
435
|
);
|
|
408
436
|
}
|
|
409
437
|
|
|
438
|
+
function NotificationCard({ notification, openSurface }: { notification: GuiNotificationSummary; openSurface: (surface: SurfaceId) => void }): ReactElement {
|
|
439
|
+
return (
|
|
440
|
+
<li className={`github-notification-card ${notification.severity} ${notification.status}`}>
|
|
441
|
+
<div className="notification-state-icon" aria-hidden="true"><NotificationIcon notification={notification} /></div>
|
|
442
|
+
<div className="notification-card-body">
|
|
443
|
+
<header>
|
|
444
|
+
<div>
|
|
445
|
+
<p className="eyebrow">{notification.category} · {notification.source}</p>
|
|
446
|
+
<h3>{notification.title}</h3>
|
|
447
|
+
</div>
|
|
448
|
+
<span className="notification-status-pill">{notification.status}</span>
|
|
449
|
+
</header>
|
|
450
|
+
<p>{notification.message}</p>
|
|
451
|
+
<small>{notification.source_ref}</small>
|
|
452
|
+
<div className="notification-actions">
|
|
453
|
+
{notification.actions.map((action) => {
|
|
454
|
+
if (action.kind === "surface" && isSurfaceId(action.surface_id)) {
|
|
455
|
+
const surfaceId = action.surface_id;
|
|
456
|
+
return <button className="secondary-action" disabled={!action.enabled} key={action.id} onClick={() => openSurface(surfaceId)} type="button">{action.label}</button>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return <button className="secondary-action" disabled key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</li>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function NotificationIcon({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
468
|
+
if (notification.severity === "error") return <FiAlertTriangle />;
|
|
469
|
+
if (notification.severity === "warning") return <FiTool />;
|
|
470
|
+
if (notification.severity === "success") return <FiCheckCircle />;
|
|
471
|
+
return <FiInfo />;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isSurfaceId(value: string | undefined): value is SurfaceId {
|
|
475
|
+
return surfaceIds.includes(value as SurfaceId);
|
|
476
|
+
}
|
|
477
|
+
|
|
410
478
|
function AdminSurface(): ReactElement {
|
|
411
479
|
return (
|
|
412
480
|
<section className="settings-surface">
|