aidevops 3.29.11 → 3.29.12

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.11-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.12-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.11
1
+ 3.29.12
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.11
8
+ # Version: 3.29.12
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.11",
3
+ "version": "3.29.12",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import * as readline from "node:readline";
2
3
  import { Hono } from "hono";
3
4
  import { APP_ACTION_ROUTE_MANIFEST, APP_ACTION_STATUS_ROUTE_MANIFEST, BANNED_ROUTE_PATTERNS, createEnvelope, FILE_EXPLORER_ROUTE_MANIFEST, type GuiAppActionId, type GuiAppActionJobSummary, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
4
5
  import { readFileExplorer } from "./file-adapter";
@@ -150,10 +151,16 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
150
151
  env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true" },
151
152
  stdio: ["ignore", "pipe", "pipe"],
152
153
  });
153
- child.stdout.on("data", (chunk) => appendJobOutput(job, chunk.toString()));
154
- child.stderr.on("data", (chunk) => appendJobOutput(job, chunk.toString()));
154
+ if (child.stdout !== null) {
155
+ const stdoutLines = readline.createInterface({ input: child.stdout, terminal: false });
156
+ stdoutLines.on("line", (line) => appendJobLine(job, line));
157
+ }
158
+ if (child.stderr !== null) {
159
+ const stderrLines = readline.createInterface({ input: child.stderr, terminal: false });
160
+ stderrLines.on("line", (line) => appendJobLine(job, line));
161
+ }
155
162
  child.on("error", (error) => {
156
- appendJobOutput(job, error.message);
163
+ appendJobLine(job, error.message);
157
164
  job.status = "failed";
158
165
  job.finished_at = new Date().toISOString();
159
166
  job.exit_code = 127;
@@ -167,8 +174,11 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
167
174
  return job;
168
175
  }
169
176
 
170
- function appendJobOutput(job: GuiAppActionJobSummary, output: string): void {
171
- job.output.push(...output.split(/\r?\n/).filter((line) => line.length > 0).map(redactOutputLine));
177
+ function appendJobLine(job: GuiAppActionJobSummary, line: string): void {
178
+ if (line.length === 0) {
179
+ return;
180
+ }
181
+ job.output.push(redactOutputLine(line));
172
182
  if (job.output.length > 400) {
173
183
  job.output.splice(1, job.output.length - 400);
174
184
  }
@@ -17,7 +17,7 @@ const toastSeverityRank: Record<GuiNotificationSeverity, number> = {
17
17
 
18
18
  const warningLineMatchers: Array<(line: string) => boolean> = [
19
19
  (line) => line.startsWith("Pulse stalled"),
20
- (line) => /contribution\(s\) need/i.test(line),
20
+ (line) => /contribution(\(s\)|s)?\s+needs?/i.test(line),
21
21
  (line) => line.startsWith("[OPENCODE MAINTENANCE]"),
22
22
  (line) => line.startsWith("[WARNING]"),
23
23
  (line) => line.startsWith("[WARN]"),
@@ -28,7 +28,7 @@ const toastTitleRules: Array<{ matches: (line: string) => boolean; title: string
28
28
  { matches: (line) => line.startsWith("[ERROR]"), title: "Startup error" },
29
29
  { matches: (line) => line.startsWith("[OPENCODE MAINTENANCE]"), title: "OpenCode maintenance" },
30
30
  { matches: (line) => line.startsWith("Pulse stalled"), title: "Pulse stalled" },
31
- { matches: (line) => /contribution\(s\) need/i.test(line), title: "External contribution review" },
31
+ { matches: (line) => /contribution(\(s\)|s)?\s+needs?/i.test(line), title: "External contribution review" },
32
32
  { matches: (line) => line.startsWith("[WARNING]") || line.startsWith("[WARN]"), title: "Startup warning" },
33
33
  { matches: (line) => line.startsWith("Security: all protections active"), title: "Security protections active" },
34
34
  ];
@@ -93,10 +93,10 @@ function buildToastNotifications(greetingOutput: string): GuiNotificationSummary
93
93
  }
94
94
 
95
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);
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
100
  const notifications: GuiNotificationSummary[] = [];
101
101
 
102
102
  if (input.restartRequired) {
@@ -222,6 +222,11 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, selecte
222
222
  ? []
223
223
  : status.opencode_sessions.sessions.filter((session) => session.repo_path_ref === selectedRepo.path_ref);
224
224
  const activeSessionId = sessions.some((session) => session.id_ref === selectedSessionId) ? selectedSessionId : sessions[0]?.id_ref;
225
+ useEffect(() => {
226
+ if (activeSessionId !== selectedSessionId) {
227
+ setSelectedSessionId(activeSessionId);
228
+ }
229
+ }, [activeSessionId, selectedSessionId, setSelectedSessionId]);
225
230
  const canSelectPreviousRepo = selectedLocalRepoIndex > 0;
226
231
  const canSelectNextRepo = selectedLocalRepoIndex < repos.length - 1;
227
232
 
@@ -189,21 +189,30 @@ function NotificationsMenu({ notifications, openSurface }: { notifications: GuiN
189
189
  <span>{active.length} active</span>
190
190
  </div>
191
191
  {preview.length === 0 ? <p>No current aidevops notifications.</p> : null}
192
- {preview.map((notification) => <NotificationPreview key={notification.id} notification={notification} />)}
192
+ {preview.map((notification) => (
193
+ <NotificationPreview
194
+ key={notification.id}
195
+ notification={notification}
196
+ onClick={() => {
197
+ const action = notification.actions.find((candidate) => candidate.enabled && candidate.kind === "surface" && isSurfaceId(candidate.surface_id));
198
+ openSurface(action?.kind === "surface" && isSurfaceId(action.surface_id) ? action.surface_id : "notifications");
199
+ }}
200
+ />
201
+ ))}
193
202
  <button onClick={() => openSurface("notifications")} role="menuitem" type="button">Open notifications</button>
194
203
  </div>
195
204
  );
196
205
  }
197
206
 
198
- function NotificationPreview({ notification }: { notification: GuiNotificationSummary }): ReactElement {
207
+ function NotificationPreview({ notification, onClick }: { notification: GuiNotificationSummary; onClick: () => void }): ReactElement {
199
208
  return (
200
- <article className={`notification-preview ${notification.severity}`}>
209
+ <button className={`notification-preview ${notification.severity}`} onClick={onClick} role="menuitem" type="button">
201
210
  <span aria-hidden="true"><NotificationIcon notification={notification} /></span>
202
211
  <div>
203
212
  <strong>{notification.title}</strong>
204
213
  <small>{notification.category} · {notification.status}</small>
205
214
  </div>
206
- </article>
215
+ </button>
207
216
  );
208
217
  }
209
218
 
@@ -528,7 +537,7 @@ function NotificationCard({ notification, openSurface }: { notification: GuiNoti
528
537
  return <button className="secondary-action" disabled={!action.enabled} key={action.id} onClick={() => openSurface(surfaceId)} type="button">{action.label}</button>;
529
538
  }
530
539
 
531
- return <button className="secondary-action" disabled key={action.id} title={action.command_preview} type="button">{action.label}</button>;
540
+ return <button className="secondary-action" disabled={!action.enabled} key={action.id} title={action.command_preview} type="button">{action.label}</button>;
532
541
  })}
533
542
  </div>
534
543
  </div>
@@ -19,6 +19,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
19
19
  const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
20
20
  const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
21
21
  const [recommendedPlatform, setRecommendedPlatform] = useState<RecommendedPlatformFilterId>("all");
22
+ const [recommendedOs, setRecommendedOs] = useState<RecommendedOsId>("all");
22
23
  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
23
24
  const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
24
25
  const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
@@ -27,7 +28,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
27
28
  const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
28
29
  const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
29
30
  const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
30
- const visibleRecommendedApps = recommendedApps.filter((app) => recommendedPlatform === "all" || app.platforms.includes(recommendedPlatform));
31
+ const visibleRecommendedApps = recommendedApps.filter((app) => recommendedAppMatchesFilters(app, recommendedPlatform, recommendedOs));
31
32
 
32
33
  useEffect(() => {
33
34
  setExpandedAppId(visibleManagedApps[0]?.id ?? null);
@@ -84,7 +85,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
84
85
  ))}
85
86
  </div>
86
87
  <p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
87
- </> : <RecommendedAppsSurface apps={visibleRecommendedApps} platform={recommendedPlatform} setPlatform={setRecommendedPlatform} />}
88
+ </> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={setRecommendedOs} setPlatform={setRecommendedPlatform} />}
88
89
  </section>
89
90
  );
90
91
  }
@@ -224,7 +225,14 @@ function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>,
224
225
  return selectedJob;
225
226
  }
226
227
 
227
- return Object.values(jobs).reverse().find((job) => job.app_id === appId) ?? null;
228
+ let latestJob: GuiAppActionJobSummary | null = null;
229
+ for (const job of Object.values(jobs)) {
230
+ if (job.app_id === appId && (latestJob === null || job.started_at > latestJob.started_at)) {
231
+ latestJob = job;
232
+ }
233
+ }
234
+
235
+ return latestJob;
228
236
  }
229
237
 
230
238
  function sortedManagedApps(apps: GuiManagedAppSummary[]): GuiManagedAppSummary[] {
@@ -303,6 +311,15 @@ const recommendedPlatformTabs: TabOption<RecommendedPlatformFilterId>[] = [
303
311
  { id: "api", label: "API" },
304
312
  ];
305
313
 
314
+ const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
315
+ { id: "all", label: "All OS" },
316
+ { id: "macos", label: "macOS" },
317
+ { id: "linux", label: "Linux" },
318
+ { id: "windows", label: "Windows" },
319
+ { id: "ios", label: "iOS" },
320
+ { id: "android", label: "Android" },
321
+ ];
322
+
306
323
  const recommendedApps = ([
307
324
  { name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"], platforms: [] },
308
325
  { name: "Bitwarden", description: "Open source password manager.", websiteUrl: "https://bitwarden.com", alternativeToUrl: "https://alternativeto.net/software/bitwarden--free-password-manager/", repoUrl: "https://github.com/bitwarden", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "cli", "api"] },
@@ -359,16 +376,19 @@ function TabNav<T extends string>({ label, onChange, tabs, value }: { label: str
359
376
  return <div aria-label={label} className="pill-tabs app-subnav" role="tablist">{tabs.map((tab) => <button aria-selected={value === tab.id} className={value === tab.id ? "active" : ""} key={tab.id} onClick={() => onChange(tab.id)} role="tab" type="button">{tab.label}</button>)}</div>;
360
377
  }
361
378
 
362
- function RecommendedAppsSurface({ apps, platform, setPlatform }: { apps: RecommendedApp[]; platform: RecommendedPlatformFilterId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
379
+ function RecommendedAppsSurface({ apps, os, platform, setOs, setPlatform }: { apps: RecommendedApp[]; os: RecommendedOsId; platform: RecommendedPlatformFilterId; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
363
380
  return <>
364
- <TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
381
+ <div className="recommended-filter-tabs">
382
+ <TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
383
+ <TabNav label="Recommended app operating system filters" tabs={recommendedOsTabs} value={os} onChange={setOs} />
384
+ </div>
365
385
  <div className="recommended-app-grid">
366
- {apps.map((app) => <RecommendedAppCard app={app} key={app.name} setPlatform={setPlatform} />)}
386
+ {apps.map((app) => <RecommendedAppCard app={app} key={app.name} setOs={setOs} setPlatform={setPlatform} />)}
367
387
  </div>
368
388
  </>;
369
389
  }
370
390
 
371
- function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
391
+ function RecommendedAppCard({ app, setOs, setPlatform }: { app: RecommendedApp; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
372
392
  return <article className="recommended-app-card">
373
393
  <div>
374
394
  <strong>{app.name}</strong>
@@ -376,7 +396,7 @@ function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlat
376
396
  </div>
377
397
  <div className="app-icon-groups">
378
398
  <PlatformIconList platforms={app.platforms} setPlatform={setPlatform} />
379
- <OsIconList os={app.os} />
399
+ <OsIconList os={app.os} setOs={setOs} />
380
400
  </div>
381
401
  <div className="managed-app-links plain-links">
382
402
  <OriginLink href={app.websiteUrl} label={text.website} />
@@ -410,15 +430,15 @@ function PlatformIcon({ id, setPlatform }: { id: RecommendedPlatformId; setPlatf
410
430
  return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setPlatform(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
411
431
  }
412
432
 
413
- function OsIconList({ os }: { os: RecommendedOsId[] }): ReactElement | null {
433
+ function OsIconList({ os, setOs }: { os: RecommendedOsId[]; setOs: (os: RecommendedOsId) => void }): ReactElement | null {
414
434
  if (os.length === 0) {
415
435
  return null;
416
436
  }
417
437
 
418
- return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} />)}</span>;
438
+ return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} setOs={setOs} />)}</span>;
419
439
  }
420
440
 
421
- function OsIcon({ id }: { id: RecommendedOsId }): ReactElement {
441
+ function OsIcon({ id, setOs }: { id: RecommendedOsId; setOs: (os: RecommendedOsId) => void }): ReactElement {
422
442
  const iconMap: Record<RecommendedOsId, { Icon: IconType; label: string }> = {
423
443
  all: { Icon: FiGlobe, label: "Web app" },
424
444
  macos: { Icon: FaApple, label: "macOS" },
@@ -429,7 +449,14 @@ function OsIcon({ id }: { id: RecommendedOsId }): ReactElement {
429
449
  };
430
450
  const { Icon, label } = iconMap[id];
431
451
 
432
- return <span aria-label={label} data-tooltip={label} role="img" title={label}><Icon aria-hidden="true" focusable="false" /></span>;
452
+ return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setOs(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
453
+ }
454
+
455
+ function recommendedAppMatchesFilters(app: RecommendedApp, platform: RecommendedPlatformFilterId, os: RecommendedOsId): boolean {
456
+ const platformMatches = platform === "all" || app.platforms.includes(platform);
457
+ const osMatches = os === "all" || app.os.includes(os);
458
+
459
+ return platformMatches && osMatches;
433
460
  }
434
461
 
435
462
  function applyManagedPolicyToggles(app: GuiManagedAppSummary, policy: Partial<Record<ManagedPolicyId, boolean>> | undefined): GuiManagedAppSummary {
@@ -13,6 +13,14 @@
13
13
  --font-family-app: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
14
14
  --font-size-app: 16px;
15
15
  --glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 58%), inset 0 -1px 0 rgb(0 0 0 / 3%), var(--shadow-soft);
16
+ --notification-error-bg: rgb(207 34 46 / 10%);
17
+ --notification-error-fg: #cf222e;
18
+ --notification-info-bg: rgb(9 105 218 / 10%);
19
+ --notification-info-fg: #0969da;
20
+ --notification-success-bg: rgb(26 127 55 / 10%);
21
+ --notification-success-fg: #1a7f37;
22
+ --notification-warning-bg: rgb(191 135 0 / 12%);
23
+ --notification-warning-fg: #9a6700;
16
24
  --shell-gutter: 8px;
17
25
  --shadow-soft: 0 18px 56px rgb(15 23 42 / 9%);
18
26
  --surface-elevated: #ffffff;
@@ -43,6 +51,14 @@
43
51
  --border-hover: color-mix(in srgb, rgb(255 255 255 / 22%) 74%, rgb(255 255 255 / 40%));
44
52
  --code-bg: hsl(var(--accent-hue) 18% 6%);
45
53
  --glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 10%), inset 0 -1px 0 rgb(0 0 0 / 24%), var(--shadow-soft);
54
+ --notification-error-bg: rgb(255 123 114 / 13%);
55
+ --notification-error-fg: #ff7b72;
56
+ --notification-info-bg: rgb(88 166 255 / 13%);
57
+ --notification-info-fg: #58a6ff;
58
+ --notification-success-bg: rgb(86 211 100 / 13%);
59
+ --notification-success-fg: #56d364;
60
+ --notification-warning-bg: rgb(210 153 34 / 15%);
61
+ --notification-warning-fg: #d29922;
46
62
  --shadow-soft: 0 24px 80px rgb(0 0 0 / 40%);
47
63
  --surface-elevated: #151515;
48
64
  --surface-muted: #202020;
@@ -1082,11 +1098,22 @@ code {
1082
1098
  }
1083
1099
 
1084
1100
  .notification-preview {
1101
+ background: var(--notification-bg, transparent);
1085
1102
  border: 1px solid var(--border);
1086
1103
  border-left: 4px solid var(--notification-color, var(--accent));
1087
1104
  border-radius: 12px;
1105
+ color: var(--text-secondary);
1106
+ cursor: pointer;
1088
1107
  gap: 10px;
1089
1108
  padding: 10px;
1109
+ text-align: left;
1110
+ width: 100%;
1111
+ }
1112
+
1113
+ .notification-preview:hover,
1114
+ .notification-preview:focus-visible {
1115
+ border-color: var(--notification-color, var(--accent));
1116
+ outline: none;
1090
1117
  }
1091
1118
 
1092
1119
  .notification-preview > span {
@@ -1106,29 +1133,29 @@ code {
1106
1133
 
1107
1134
  .notification-preview.error,
1108
1135
  .github-notification-card.error {
1109
- --notification-color: #cf222e;
1110
- --notification-bg: rgb(207 34 46 / 10%);
1136
+ --notification-color: var(--notification-error-fg);
1137
+ --notification-bg: var(--notification-error-bg);
1111
1138
  }
1112
1139
 
1113
1140
  .notification-preview.warning,
1114
1141
  .github-notification-card.warning {
1115
- --notification-color: #9a6700;
1116
- --notification-bg: rgb(191 135 0 / 12%);
1142
+ --notification-color: var(--notification-warning-fg);
1143
+ --notification-bg: var(--notification-warning-bg);
1117
1144
  }
1118
1145
 
1119
1146
  .notification-preview.info,
1120
1147
  .github-notification-card.info {
1121
- --notification-color: #0969da;
1122
- --notification-bg: rgb(9 105 218 / 10%);
1148
+ --notification-color: var(--notification-info-fg);
1149
+ --notification-bg: var(--notification-info-bg);
1123
1150
  }
1124
1151
 
1125
1152
  .notification-preview.success,
1126
1153
  .github-notification-card.success {
1127
- --notification-color: #1a7f37;
1128
- --notification-bg: rgb(26 127 55 / 10%);
1154
+ --notification-color: var(--notification-success-fg);
1155
+ --notification-bg: var(--notification-success-bg);
1129
1156
  }
1130
1157
 
1131
- .popover-menu button,
1158
+ .popover-menu button:not(.notification-preview),
1132
1159
  .popover-menu a {
1133
1160
  align-items: center;
1134
1161
  background: transparent;
@@ -1142,9 +1169,9 @@ code {
1142
1169
  text-decoration: none;
1143
1170
  }
1144
1171
 
1145
- .popover-menu button:hover:not(:disabled),
1172
+ .popover-menu button:not(.notification-preview):hover:not(:disabled),
1146
1173
  .popover-menu a:hover,
1147
- .popover-menu button:focus-visible,
1174
+ .popover-menu button:not(.notification-preview):focus-visible,
1148
1175
  .popover-menu a:focus-visible {
1149
1176
  background: var(--surface-muted);
1150
1177
  color: var(--accent);
@@ -2496,6 +2523,12 @@ button.managed-toggle:focus-visible {
2496
2523
  justify-self: start;
2497
2524
  }
2498
2525
 
2526
+ .recommended-filter-tabs {
2527
+ display: flex;
2528
+ flex-wrap: wrap;
2529
+ gap: 10px;
2530
+ }
2531
+
2499
2532
  .recommended-app-grid {
2500
2533
  display: grid;
2501
2534
  gap: 12px;
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.11
15
+ # Version: 3.29.12
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)