aidevops 3.29.5 → 3.29.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
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.29.5-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.6-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.29.5
1
+ 3.29.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.29.5
8
+ # Version: 3.29.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.29.5",
3
+ "version": "3.29.6",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -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: readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef),
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",
@@ -1,2 +1,3 @@
1
1
  export * from "./contracts";
2
2
  export * from "./fixtures";
3
+ export * from "./notifications";
@@ -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
- <strong>Notifications</strong>
183
- <p>Local readiness updates, release activity, and account alerts will collect here.</p>
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>Release updates, workflow failures, security notices, and hosted account messages will appear here.</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
- <li><strong>Readiness</strong><span>Local status and restart notices.</span></li>
403
- <li><strong>Workflow</strong><span>PR, release, and routine activity.</span></li>
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">
@@ -330,6 +330,10 @@ function plannedNavItem(id: SurfaceId, label: string, description: string, icon:
330
330
  return { id, label, description, icon, badge: text.planned };
331
331
  }
332
332
 
333
+ function plannedCommsGroup(label: string, items: SurfaceNavItem[]): SurfaceNavGroup {
334
+ return { label, mode: "comms", items };
335
+ }
336
+
333
337
  export const navGroups: SurfaceNavGroup[] = [
334
338
  {
335
339
  label: text.development,
@@ -388,36 +392,28 @@ export const navGroups: SurfaceNavGroup[] = [
388
392
  { id: "marketplaces", label: text.marketplaces, description: "Storefronts and seller profiles", icon: "package", badge: text.planned },
389
393
  ],
390
394
  },
391
- {
392
- label: text.management,
393
- mode: "comms",
394
- items: [
395
- plannedNavItem("channels", text.channels, "Team channels", "hash"),
396
- plannedNavItem("directMessages", text.directMessages, "Direct and group DMs", "message"),
397
- { id: "emailAccounts", label: text.emailAccounts, description: "Mail account sync", icon: "mail", badge: text.planned },
398
- { id: "messagingAccounts", label: text.messagingAccounts, description: "Messaging account sync", icon: "message", badge: text.planned },
399
- { id: "events", label: text.calendars, description: "CalDAV calendar sync", icon: "calendar", badge: text.planned },
400
- { id: "contacts", label: text.contacts, description: "CardDAV contact sync", icon: "users", badge: text.planned },
401
- { id: "tasks", label: text.tasks, description: "CalDAV task sync", icon: "list", badge: text.planned },
402
- { id: "notes", label: text.notes, description: "Synced notes", icon: "note", badge: text.planned },
403
- { id: "bookmarks", label: text.bookmarks, description: "Saved links and references", icon: "bookmark", badge: text.planned },
404
- ],
405
- },
406
- {
407
- label: text.projectWork,
408
- mode: "comms",
409
- items: [
410
- { id: "inbox", label: text.inbox, description: "Project intake", icon: "folder", badge: text.planned },
411
- { id: "campaigns", label: text.campaigns, description: "Campaign folders", icon: "folder", badge: text.planned },
412
- { id: "cases", label: text.cases, description: "Case folders", icon: "folder", badge: text.planned },
413
- { id: "projectConfig", label: text.config, description: "Project configuration", icon: "folder", badge: text.planned },
414
- { id: "feedback", label: text.feedback, description: "Feedback folders", icon: "folder", badge: text.planned },
415
- { id: "knowledge", label: text.knowledge, description: "Knowledge folders", icon: "folder", badge: text.planned },
416
- { id: "maintenance", label: text.maintenance, description: "Maintenance folders", icon: "folder", badge: text.planned },
417
- { id: "performance", label: text.performance, description: "Performance folders", icon: "folder", badge: text.planned },
418
- { id: "reports", label: text.reports, description: "Report folders", icon: "folder", badge: text.planned },
419
- ],
420
- },
395
+ plannedCommsGroup(text.management, [
396
+ plannedNavItem("channels", text.channels, "Team channels", "hash"),
397
+ plannedNavItem("directMessages", text.directMessages, "Direct and group DMs", "message"),
398
+ plannedNavItem("emailAccounts", text.emailAccounts, "Mail account sync", "mail"),
399
+ plannedNavItem("messagingAccounts", text.messagingAccounts, "Messaging account sync", "message"),
400
+ plannedNavItem("events", text.calendars, "CalDAV calendar sync", "calendar"),
401
+ plannedNavItem("contacts", text.contacts, "CardDAV contact sync", "users"),
402
+ plannedNavItem("tasks", text.tasks, "CalDAV task sync", "list"),
403
+ plannedNavItem("notes", text.notes, "Synced notes", "note"),
404
+ plannedNavItem("bookmarks", text.bookmarks, "Saved links and references", "bookmark"),
405
+ ]),
406
+ plannedCommsGroup(text.projectWork, [
407
+ plannedNavItem("inbox", text.inbox, "Project intake", "folder"),
408
+ plannedNavItem("campaigns", text.campaigns, "Campaign folders", "folder"),
409
+ plannedNavItem("cases", text.cases, "Case folders", "folder"),
410
+ plannedNavItem("projectConfig", text.config, "Project configuration", "folder"),
411
+ plannedNavItem("feedback", text.feedback, "Feedback folders", "folder"),
412
+ plannedNavItem("knowledge", text.knowledge, "Knowledge folders", "folder"),
413
+ plannedNavItem("maintenance", text.maintenance, "Maintenance folders", "folder"),
414
+ plannedNavItem("performance", text.performance, "Performance folders", "folder"),
415
+ plannedNavItem("reports", text.reports, "Report folders", "folder"),
416
+ ]),
421
417
  ];
422
418
 
423
419
  export const dashboardNavItem: SurfaceNavItem = {
@@ -607,6 +603,7 @@ export function surfaceRecordCounts(status: GuiStatusData): SurfaceRecordCounts
607
603
  hosts: populatedInventoryRowCount("hosts"),
608
604
  installation: installationRows.length,
609
605
  localSetup: status.paths.length,
606
+ notifications: status.notifications.filter((notification) => notification.status === "active").length,
610
607
  projects: status.repos.total || status.repos.repos.length,
611
608
  repos: (status.local_repos.total || status.local_repos.repos.length) + (status.repos.total || status.repos.repos.length),
612
609
  registrars: populatedInventoryRowCount("registrars"),
@@ -55,7 +55,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
55
55
  `<h1>aidevops app interface</h1>`,
56
56
  `<p>Made for creators.</p>`,
57
57
  `<p>AI-assisted development workflows, code quality, and deployment automation.</p>`,
58
- `<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Menlo (default), Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. The workspace header includes a command palette, reserved signposts/help slot immediately left of notifications, notifications menu, AI Assistant panel, and profile menu with Settings, Theme, Language, Community, Admin, and Logout entries. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
58
+ `<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Menlo (default), Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. The workspace header includes a command palette, reserved signposts/help slot immediately left of notifications, notifications menu, AI Assistant panel, and profile menu with Settings, Theme, Language, Community, Admin, and Logout entries. Notifications reuse the OpenCode toast status cache and GUI readiness data. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
59
59
  `<p>Secret references stay read-only and redacted.</p>`,
60
60
  `<p>Vault surfaces show padlock indicators. Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.</p>`,
61
61
  `<p>${escapeHtml(status.data.update.message)}</p>`,
@@ -87,6 +87,7 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
87
87
  setup_targets: data.setup_targets ?? statusFixture.setup_targets,
88
88
  ai_apps: data.ai_apps ?? statusFixture.ai_apps,
89
89
  managed_apps: data.managed_apps ?? statusFixture.managed_apps,
90
+ notifications: data.notifications ?? statusFixture.notifications,
90
91
  vault: {
91
92
  ...statusFixture.vault,
92
93
  ...data.vault,
@@ -1044,6 +1044,78 @@ code {
1044
1044
  min-width: 240px;
1045
1045
  }
1046
1046
 
1047
+ .notifications-menu {
1048
+ min-width: 320px;
1049
+ }
1050
+
1051
+ .notifications-menu-heading,
1052
+ .notification-preview {
1053
+ align-items: center;
1054
+ display: flex;
1055
+ }
1056
+
1057
+ .notifications-menu-heading {
1058
+ justify-content: space-between;
1059
+ padding: 2px 4px 6px;
1060
+ }
1061
+
1062
+ .notifications-menu-heading span {
1063
+ background: var(--surface-muted);
1064
+ border-radius: 999px;
1065
+ color: var(--text-muted);
1066
+ font-size: 0.76rem;
1067
+ font-weight: 900;
1068
+ padding: 4px 8px;
1069
+ text-transform: uppercase;
1070
+ }
1071
+
1072
+ .notification-preview {
1073
+ border: 1px solid var(--border);
1074
+ border-left: 4px solid var(--notification-color, var(--accent));
1075
+ border-radius: 12px;
1076
+ gap: 10px;
1077
+ padding: 10px;
1078
+ }
1079
+
1080
+ .notification-preview > span {
1081
+ color: var(--notification-color, var(--accent));
1082
+ }
1083
+
1084
+ .notification-preview strong,
1085
+ .notification-preview small {
1086
+ display: block;
1087
+ }
1088
+
1089
+ .notification-preview small {
1090
+ color: var(--text-muted);
1091
+ font-size: 0.74rem;
1092
+ text-transform: uppercase;
1093
+ }
1094
+
1095
+ .notification-preview.error,
1096
+ .github-notification-card.error {
1097
+ --notification-color: #cf222e;
1098
+ --notification-bg: rgb(207 34 46 / 10%);
1099
+ }
1100
+
1101
+ .notification-preview.warning,
1102
+ .github-notification-card.warning {
1103
+ --notification-color: #9a6700;
1104
+ --notification-bg: rgb(191 135 0 / 12%);
1105
+ }
1106
+
1107
+ .notification-preview.info,
1108
+ .github-notification-card.info {
1109
+ --notification-color: #0969da;
1110
+ --notification-bg: rgb(9 105 218 / 10%);
1111
+ }
1112
+
1113
+ .notification-preview.success,
1114
+ .github-notification-card.success {
1115
+ --notification-color: #1a7f37;
1116
+ --notification-bg: rgb(26 127 55 / 10%);
1117
+ }
1118
+
1047
1119
  .popover-menu button,
1048
1120
  .popover-menu a {
1049
1121
  align-items: center;
@@ -1440,6 +1512,112 @@ code {
1440
1512
  color: var(--text-muted);
1441
1513
  }
1442
1514
 
1515
+ .notifications-hero {
1516
+ background:
1517
+ linear-gradient(135deg, var(--notification-bg, color-mix(in srgb, var(--accent) 12%, transparent)), transparent 62%),
1518
+ var(--surface-elevated);
1519
+ }
1520
+
1521
+ .notification-summary-strip {
1522
+ display: flex;
1523
+ flex-wrap: wrap;
1524
+ gap: 8px;
1525
+ margin-top: 16px;
1526
+ }
1527
+
1528
+ .notification-summary-strip span,
1529
+ .notification-status-pill {
1530
+ background: var(--surface-muted);
1531
+ border: 1px solid var(--border);
1532
+ border-radius: 999px;
1533
+ color: var(--text-secondary);
1534
+ font-size: 0.8rem;
1535
+ font-weight: 800;
1536
+ padding: 6px 10px;
1537
+ }
1538
+
1539
+ .notification-summary-strip strong {
1540
+ color: var(--accent);
1541
+ }
1542
+
1543
+ .github-notification-list {
1544
+ display: grid;
1545
+ gap: 12px;
1546
+ list-style: none;
1547
+ margin: 0;
1548
+ padding: 0;
1549
+ }
1550
+
1551
+ .github-notification-card {
1552
+ background: var(--surface-elevated);
1553
+ border: 1px solid var(--border);
1554
+ border-left: 5px solid var(--notification-color, var(--accent));
1555
+ border-radius: 18px;
1556
+ box-shadow: var(--glass-panel-shadow);
1557
+ display: grid;
1558
+ gap: 12px;
1559
+ grid-template-columns: auto minmax(0, 1fr);
1560
+ padding: 16px;
1561
+ }
1562
+
1563
+ .github-notification-card.resolved {
1564
+ opacity: 0.78;
1565
+ }
1566
+
1567
+ .notification-state-icon {
1568
+ align-items: center;
1569
+ background: var(--notification-bg, var(--success-bg));
1570
+ border-radius: 999px;
1571
+ color: var(--notification-color, var(--accent));
1572
+ display: inline-flex;
1573
+ height: 38px;
1574
+ justify-content: center;
1575
+ width: 38px;
1576
+ }
1577
+
1578
+ .notification-state-icon svg {
1579
+ height: 20px;
1580
+ stroke-width: 2.5;
1581
+ width: 20px;
1582
+ }
1583
+
1584
+ .notification-card-body {
1585
+ display: grid;
1586
+ gap: 10px;
1587
+ min-width: 0;
1588
+ }
1589
+
1590
+ .notification-card-body header {
1591
+ align-items: start;
1592
+ display: flex;
1593
+ gap: 12px;
1594
+ justify-content: space-between;
1595
+ }
1596
+
1597
+ .notification-card-body h3,
1598
+ .notification-card-body p {
1599
+ margin: 0;
1600
+ }
1601
+
1602
+ .notification-card-body p {
1603
+ color: var(--text-secondary);
1604
+ }
1605
+
1606
+ .notification-card-body small {
1607
+ color: var(--text-muted);
1608
+ overflow-wrap: anywhere;
1609
+ }
1610
+
1611
+ .notification-actions {
1612
+ display: flex;
1613
+ flex-wrap: wrap;
1614
+ gap: 8px;
1615
+ }
1616
+
1617
+ .notification-actions .secondary-action {
1618
+ padding: 8px 10px;
1619
+ }
1620
+
1443
1621
  .metric-card span,
1444
1622
  .planned-home span,
1445
1623
  .data-row.header-row,
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.29.5
15
+ # Version: 3.29.6
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)