aidevops 3.29.40 → 3.29.42

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.
@@ -1,5 +1,6 @@
1
- import { type CSSProperties, type ReactElement, useState } from "react";
2
- import type { GuiPulseWorkerActivityEvent, GuiPulseWorkerChartSeries, GuiStatusData } from "../../gui-shared/src";
1
+ import { type CSSProperties, type Dispatch, type ReactElement, type SetStateAction, useEffect, useState } from "react";
2
+ import type { GuiPulseWorkerActionJobSummary, GuiPulseWorkerActionSummary, GuiPulseWorkerActivityEvent, GuiPulseWorkerChartSeries, GuiResponseEnvelope, GuiStatusData } from "../../gui-shared/src";
3
+ import { AppActionTerminal } from "./AppActionTerminal";
3
4
  import { text } from "./app-model";
4
5
 
5
6
  type PulseEvent = GuiPulseWorkerActivityEvent;
@@ -10,10 +11,32 @@ const quickFilters = ["Needs attention", "Stalled workers", "Failed terminal che
10
11
  export function PulseWorkersSurface({ status }: { status: GuiStatusData }): ReactElement {
11
12
  const pulse = status.pulse_workers;
12
13
  const [selectedEventId, setSelectedEventId] = useState(pulse.events[0]?.id ?? "");
14
+ const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
15
+ const [jobs, setJobs] = useState<Record<string, GuiPulseWorkerActionJobSummary>>({});
16
+ const [dismissedJobIds, setDismissedJobIds] = useState<Set<string>>(() => new Set());
13
17
  const selectedEvent = pulse.events.find((event) => event.id === selectedEventId) ?? pulse.events[0];
18
+ const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
14
19
  const filterGroups = buildFilterGroups(pulse);
15
20
  const chartSeries = pulse.charts.slice(0, 4);
16
21
  const sampleSize = pulse.kpis.reduce((total, kpi) => total + (kpi.sample_size ?? 0), 0);
22
+ const runningJobIdsKey = Object.values(jobs).filter((job) => job.status === "running").map((job) => job.id).sort().join("|");
23
+
24
+ useEffect(() => {
25
+ const runningJobIds = runningJobIdsKey.split("|").filter(Boolean);
26
+ if (runningJobIds.length === 0) {
27
+ return undefined;
28
+ }
29
+
30
+ const refreshRunningJobs = () => {
31
+ for (const jobId of runningJobIds) {
32
+ void refreshPulseWorkerJob(jobId, setJobs);
33
+ }
34
+ };
35
+ refreshRunningJobs();
36
+ const timer = window.setInterval(refreshRunningJobs, 1_500);
37
+
38
+ return () => window.clearInterval(timer);
39
+ }, [runningJobIdsKey]);
17
40
 
18
41
  return (
19
42
  <section className="pulse-workers-surface" aria-label={text.workers}>
@@ -49,12 +72,27 @@ export function PulseWorkersSurface({ status }: { status: GuiStatusData }): Reac
49
72
  <p className="eyebrow">Exceptions first</p>
50
73
  <h3>Needs attention</h3>
51
74
  </div>
52
- <span className="count-pill">{pulse.attention.length} findings · planned actions disabled</span>
75
+ <span className="count-pill">{pulse.attention.length} findings · allowlisted actions only</span>
53
76
  </div>
54
77
  <ul>
55
78
  {pulse.attention.map((item) => <li className={`pulse-attention-${item.severity}`} key={item.id}><strong>{item.title}</strong>: {item.detail}</li>)}
56
79
  </ul>
57
- <button disabled title="Action routes need audited worker control APIs" type="button">Create systemic fix (planned)</button>
80
+ <div className="pulse-insight-list" aria-label="Grouped systemic findings">
81
+ {pulse.insights.map((finding) => (
82
+ <article className={`pulse-insight-card pulse-attention-${finding.severity}`} key={finding.id}>
83
+ <p className="eyebrow">{humanize(finding.kind)} · {finding.confidence} confidence · n={finding.sample_size}</p>
84
+ <h4>{finding.title}</h4>
85
+ <p>{finding.detail}</p>
86
+ <dl className="pulse-detail-grid compact-detail-grid">
87
+ <div><dt>Likely cause</dt><dd>{finding.likely_cause}</dd></div>
88
+ <div><dt>Evidence</dt><dd>{finding.evidence_refs.join(" · ") || "metadata evidence pending"}</dd></div>
89
+ <div><dt>Comparison</dt><dd>{finding.period_label} · {finding.scope_label} · {finding.comparison_label}</dd></div>
90
+ <div><dt>Systemic fix</dt><dd>{finding.recommendation}</dd></div>
91
+ </dl>
92
+ </article>
93
+ ))}
94
+ </div>
95
+ <button disabled={!actionById(pulse.actions, "create_systemic_fix")?.enabled} onClick={() => void runPulseWorkerAction(actionById(pulse.actions, "create_systemic_fix"), setJobs, setSelectedJobId, setDismissedJobIds)} title="Create a worker-ready systemic-fix task from selected evidence" type="button">Create systemic fix task</button>
58
96
  </article>
59
97
 
60
98
  <section className="pulse-chart-grid" aria-label="Pulse trend charts">
@@ -120,23 +158,66 @@ export function PulseWorkersSurface({ status }: { status: GuiStatusData }): Reac
120
158
  </button>
121
159
  ))}
122
160
  </div>
123
- <p className="notice compact-notice">Mobile activity cards replace the dense table on small screens. Detail drawer becomes a full-screen sheet on small screens, and terminal panel becomes full-screen later.</p>
161
+ <p className="notice compact-notice">Mobile activity cards replace the dense table on small screens. Detail drawer becomes a full-screen sheet on small screens, and terminal panel becomes a full-screen panel/sheet for command output.</p>
124
162
  </section>
125
163
 
126
164
  <section className="pulse-layout" aria-label="Drilldown and planned actions">
127
165
  <PulseDrilldownPanel event={selectedEvent} />
128
- <article className="planned-card pulse-actions-panel">
129
- <p className="eyebrow">Planned controls</p>
130
- <h3>Actions stay disabled until audited routes land</h3>
131
- <button disabled title="Terminal output needs a read-only command-output adapter" type="button">Open terminal output (planned)</button>
132
- <button disabled title="Dispatch needs worker control and trust-boundary APIs" type="button">Redispatch worker (planned)</button>
133
- <button disabled title="Persistence needs a write-action manifest and audit trail" type="button">Save systemic fix (planned)</button>
166
+ <article className="planned-card pulse-actions-panel" aria-label="Pulse and Workers action buttons">
167
+ <p className="eyebrow">Allowlisted controls</p>
168
+ <h3>Safe actions with terminal output</h3>
169
+ <p className="notice compact-notice">Read-only diagnostics are separated from write actions. Destructive controls such as stopping workers, closing PRs, cleanup, and rebase/update branch remain deferred.</p>
170
+ <div className="pulse-action-list">
171
+ {pulse.actions.map((action) => (
172
+ <button disabled={!action.enabled} key={action.id} onClick={() => void runPulseWorkerAction(action, setJobs, setSelectedJobId, setDismissedJobIds)} title={action.scope_copy} type="button">
173
+ <strong>{action.label}</strong>
174
+ <span>{action.classification} · confirmation {action.confirmation} · {action.target_ref}</span>
175
+ </button>
176
+ ))}
177
+ </div>
178
+ <dl className="pulse-detail-grid pulse-action-confirmations">
179
+ {pulse.actions.map((action) => <div key={`${action.id}-copy`}><dt>{action.label}</dt><dd>{action.scope_copy} {action.expected_effect} Command preview: {action.command_preview}. Audit/source ref: {action.audit_ref}.</dd></div>)}
180
+ </dl>
181
+ <p className="notice compact-notice">Create systemic fix task generates worker-ready context from the selected event: files/patterns when known, evidence refs, verification commands, and protected-data notes before wrapper dispatch.</p>
182
+ {selectedJob === null || dismissedJobIds.has(selectedJob.id) ? null : <AppActionTerminal job={selectedJob} label={`Pulse & Workers ${selectedJob.action}`} onDismiss={(jobId) => setDismissedJobIds((current) => new Set([...current, jobId]))} />}
134
183
  </article>
135
184
  </section>
136
185
  </section>
137
186
  );
138
187
  }
139
188
 
189
+ async function refreshPulseWorkerJob(jobId: string, setJobs: Dispatch<SetStateAction<Record<string, GuiPulseWorkerActionJobSummary>>>): Promise<void> {
190
+ const response = await fetch(`/api/pulse-workers/jobs/${encodeURIComponent(jobId)}`);
191
+ if (!response.ok) {
192
+ return;
193
+ }
194
+ const envelope = await response.json() as GuiResponseEnvelope<GuiPulseWorkerActionJobSummary>;
195
+ setJobs((current) => ({ ...current, [envelope.data.id]: envelope.data }));
196
+ }
197
+
198
+ async function runPulseWorkerAction(action: GuiPulseWorkerActionSummary | undefined, setJobs: Dispatch<SetStateAction<Record<string, GuiPulseWorkerActionJobSummary>>>, setSelectedJobId: Dispatch<SetStateAction<string | null>>, setDismissedJobIds: Dispatch<SetStateAction<Set<string>>>): Promise<void> {
199
+ if (action === undefined || !action.enabled) {
200
+ return;
201
+ }
202
+ if (action.confirmation === "required" && typeof window !== "undefined" && !window.confirm(`${action.label}\n\nScope: ${action.scope_copy}\nExpected effect: ${action.expected_effect}\nCommand: ${action.command_preview}`)) {
203
+ return;
204
+ }
205
+
206
+ const response = await fetch(`/api/pulse-workers/actions/${encodeURIComponent(action.id)}`, { method: "POST" });
207
+ const envelope = await response.json() as GuiResponseEnvelope<GuiPulseWorkerActionJobSummary>;
208
+ setDismissedJobIds((current) => {
209
+ const next = new Set(current);
210
+ next.delete(envelope.data.id);
211
+ return next;
212
+ });
213
+ setJobs((current) => ({ ...current, [envelope.data.id]: envelope.data }));
214
+ setSelectedJobId(envelope.data.id);
215
+ }
216
+
217
+ function actionById(actions: GuiPulseWorkerActionSummary[], id: GuiPulseWorkerActionSummary["id"]): GuiPulseWorkerActionSummary | undefined {
218
+ return actions.find((action) => action.id === id);
219
+ }
220
+
140
221
  function PulseChartPanel({ chart }: { chart: GuiPulseWorkerChartSeries }): ReactElement {
141
222
  const maxValue = Math.max(...chart.points.map((point) => point.value), 1);
142
223
 
@@ -170,19 +251,24 @@ function PulseDrilldownPanel({ event }: { event: PulseEvent | undefined }): Reac
170
251
  <dl className="pulse-detail-grid">
171
252
  <div><dt>What</dt><dd>{event.summary}</dd></div>
172
253
  <div><dt>When</dt><dd>{event.occurred_at} · {durationLabel(event.duration_ms)}</dd></div>
173
- <div><dt>Why</dt><dd>{event.drilldown_sections.find((section) => section.label.toLowerCase().includes("failure"))?.body ?? event.drilldown_sections[0]?.body ?? "No failure analysis recorded."}</dd></div>
254
+ <div><dt>Why</dt><dd>{event.drilldown_sections?.find((section) => section.label.toLowerCase().includes("failure"))?.body ?? event.drilldown_sections?.[0]?.body ?? "No failure analysis recorded."}</dd></div>
174
255
  <div><dt>How</dt><dd>{[event.type, event.status, event.outcome].map((value) => humanize(value)).join(" · ")}</dd></div>
175
256
  <div><dt>Who</dt><dd>{event.actor_ref ?? "actor pending"} · {humanize(event.issue_origin, "-")} · {event.author_association}</dd></div>
176
257
  <div><dt>Issue / PR / session refs</dt><dd>{[event.repo_ref, event.issue_ref, event.pull_request_ref, event.worker_session_ref, event.command_job_ref, event.pulse_run_ref].filter(Boolean).join(" · ")}</dd></div>
177
258
  <div><dt>Usage and cost</dt><dd>{usageSummary(event)}</dd></div>
178
- <div><dt>Resources</dt><dd>{event.resources.map((resource) => `${resource.label}: ${resource.available_label} (${resource.pressure})`).join(" · ") || "Resource metadata pending"}</dd></div>
179
- <div><dt>Suggested systemic fix</dt><dd>{event.drilldown_sections.find((section) => section.label.toLowerCase().includes("fix"))?.body ?? "Create a worker-ready follow-up when repeated blindspots appear."}</dd></div>
259
+ <div><dt>Resources</dt><dd>{event.resources?.map((resource) => `${resource.label}: ${resource.available_label} (${resource.pressure})`).join(" · ") || "Resource metadata pending"}</dd></div>
260
+ <div><dt>Suggested systemic fix</dt><dd>{event.drilldown_sections?.find((section) => section.label.toLowerCase().includes("fix"))?.body ?? "Create a worker-ready follow-up when repeated blindspots appear."}</dd></div>
261
+ <div><dt>Related systemic findings</dt><dd>{relatedFindings(event).join(" · ") || "No grouped finding attached to this event."}</dd></div>
180
262
  <div><dt>Planned actions</dt><dd>Open terminal output, redispatch worker, save systemic fix, and attach timeline evidence remain disabled until Phase 5 action routes land.</dd></div>
181
263
  </dl>
182
264
  </article>
183
265
  );
184
266
  }
185
267
 
268
+ function relatedFindings(event: PulseEvent): string[] {
269
+ return event.drilldown_sections?.filter((section) => section.label.toLowerCase().includes("systemic") || section.label.toLowerCase().includes("verification")).map((section) => `${section.label}: ${section.body}`) ?? [];
270
+ }
271
+
186
272
  function buildFilterGroups(pulse: GuiStatusData["pulse_workers"]): PulseFilterGroup[] {
187
273
  return [
188
274
  { label: "Repo", values: optionLabels(pulse.filters.repos) },
@@ -201,15 +287,15 @@ function buildFilterGroups(pulse: GuiStatusData["pulse_workers"]): PulseFilterGr
201
287
  ];
202
288
  }
203
289
 
204
- function optionLabels(options: Array<{ label: string; count: number }>): string[] {
205
- return options.map((option) => `${option.label} (${option.count})`);
290
+ function optionLabels(options: Array<{ label: string; count: number }> | null | undefined): string[] {
291
+ return options?.map((option) => `${option.label} (${option.count})`) ?? [];
206
292
  }
207
293
 
208
294
  function unique(values: string[]): string[] {
209
295
  return Array.from(new Set(values.filter(Boolean)));
210
296
  }
211
297
 
212
- function filterSummary(options: Array<{ label: string; count: number }>): string {
298
+ function filterSummary(options: Array<{ label: string; count: number }> | null | undefined): string {
213
299
  return optionLabels(options).slice(0, 3).join(" · ") || "all";
214
300
  }
215
301
 
@@ -220,7 +306,7 @@ function periodChoices(selected: string): string {
220
306
  function resourceSummary(row: PulseEvent): string {
221
307
  const model = row.usage?.model_ref?.replace("model:", "");
222
308
  const providerLabel = row.usage?.provider === "openai" ? "OpenAI" : row.usage?.provider === "anthropic" ? "Anthropic" : row.usage?.provider;
223
- const provider = row.usage === null ? row.resources[0]?.available_label ?? "metadata only" : `${providerLabel ?? "Provider"} · ${model ?? "model metadata pending"}`;
309
+ const provider = row.usage === null ? row.resources?.[0]?.available_label ?? "metadata only" : `${providerLabel ?? "Provider"} · ${model ?? "model metadata pending"}`;
224
310
  const tokens = row.usage === null ? "" : ` · ${row.usage.total_tokens.toLocaleString()} tokens`;
225
311
  const cost = row.usage?.estimated_cost_ref === null || row.usage?.estimated_cost_ref === undefined ? "" : ` · ${row.usage.estimated_cost_ref}`;
226
312
 
@@ -257,6 +343,10 @@ function durationLabel(durationMs: number | null): string {
257
343
  return `${Math.round(durationMs / 60000)}m`;
258
344
  }
259
345
 
260
- function humanize(value: string, replacement = " "): string {
346
+ function humanize(value: string | null | undefined, replacement = " "): string {
347
+ if (!value) {
348
+ return "";
349
+ }
350
+
261
351
  return value.replaceAll("_", replacement);
262
352
  }
@@ -148,6 +148,13 @@ export interface InventorySurfaceConfig {
148
148
  title: string;
149
149
  }
150
150
 
151
+ export interface ChatPrimitiveStackDecision {
152
+ adopt: string[];
153
+ defer: string[];
154
+ foundation: string;
155
+ owner: "local_vite_shell";
156
+ }
157
+
151
158
  export type SurfaceRecordCounts = Partial<Record<SurfaceId, number>>;
152
159
 
153
160
  export const text = {
@@ -278,6 +285,8 @@ export const text = {
278
285
  workersIntro: "Pulse & Workers brings worker sessions, event streams, outcomes, resources, and systemic fixes into one observability console so attention goes to exceptions, bottlenecks, and reusable improvements instead of raw noise.",
279
286
  theme: "Theme",
280
287
  appearance: "Appearance",
288
+ approvalExecutionDeferred: "Approval execution is deferred until audited approval tooling exists.",
289
+ devOpsCard: "DevOps card",
281
290
  hue: "Hue",
282
291
  reset: "Reset",
283
292
  showBorders: "Show borders",
@@ -286,6 +295,8 @@ export const text = {
286
295
  fontSize: "Font size",
287
296
  truncated: "Preview truncated for safety.",
288
297
  update: "Update",
298
+ tamboGenUiReadOnly: "Tambo GenUI · read-only",
299
+ unsupportedDevOpsCard: "Unsupported DevOps card",
289
300
  vault: "Vault",
290
301
  vaultAudit: "Audit Logs",
291
302
  vaultBackups: "Backups & Recovery",
@@ -315,6 +326,13 @@ export const DEFAULT_CONTRAST: ContrastPreference = "low";
315
326
  export const DEFAULT_FONT: FontPreference = "Inter";
316
327
  export const DEFAULT_FONT_SIZE: FontSizePreference = "xs";
317
328
 
329
+ export const chatPrimitiveStackDecision: ChatPrimitiveStackDecision = {
330
+ owner: "local_vite_shell",
331
+ foundation: "Turbostarter AI patterns supply persistence, AI SDK transport, model selection, attachments, tool/reasoning panels, sharing, and session actions behind audited routes.",
332
+ adopt: ["MessageScroller", "Message", "Bubble", "Attachment", "Marker"],
333
+ defer: ["Tambo ordinary chat persistence", "Tambo channel transport", "React Joyride as chat infrastructure", "Next.js route ownership"],
334
+ };
335
+
318
336
  export const contrastOptions: ContrastOption[] = [
319
337
  { value: "low", label: "Low" },
320
338
  { value: "medium", label: "Medium" },
@@ -368,7 +386,7 @@ export const navGroups: SurfaceNavGroup[] = [
368
386
  mode: "devops",
369
387
  items: [
370
388
  plannedNavItem("aiSessions", text.aiSessions, "AI chat and OpenCode sessions", "terminal"),
371
- plannedNavItem("workers", text.workers, "Pulse, worker sessions, outcomes, and resources", "activity"),
389
+ { id: "workers", label: text.workers, description: "Pulse, worker sessions, outcomes, and resources", icon: "activity" },
372
390
  { id: "git", label: text.localRepos, description: "~/Git explorer", icon: "folder" },
373
391
  plannedNavItem("repos", text.repos, "Unified local and remote repo context", "git"),
374
392
  { id: "projects", label: text.projects, description: "repos.json summary", icon: "git" },
@@ -104,9 +104,11 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
104
104
  ...data.pulse_workers,
105
105
  kpis: data.pulse_workers?.kpis ?? statusFixture.pulse_workers.kpis,
106
106
  attention: data.pulse_workers?.attention ?? statusFixture.pulse_workers.attention,
107
+ insights: data.pulse_workers?.insights ?? statusFixture.pulse_workers.insights,
107
108
  filters: { ...statusFixture.pulse_workers.filters, ...data.pulse_workers?.filters },
108
109
  charts: data.pulse_workers?.charts ?? statusFixture.pulse_workers.charts,
109
110
  events: data.pulse_workers?.events ?? statusFixture.pulse_workers.events,
111
+ actions: data.pulse_workers?.actions ?? statusFixture.pulse_workers.actions,
110
112
  },
111
113
  capabilities: data.capabilities ?? statusFixture.capabilities,
112
114
  secrets: data.secrets ?? statusFixture.secrets,
@@ -50,10 +50,10 @@
50
50
  --bg-primary: #0a0a0a;
51
51
  --bg-secondary: #111111;
52
52
  --bg-tertiary: #1a1a1a;
53
- --border: color-mix(in srgb, rgb(255 255 255 / 11%) 76%, rgb(255 255 255 / 30%));
54
- --border-hover: color-mix(in srgb, rgb(255 255 255 / 22%) 74%, rgb(255 255 255 / 40%));
53
+ --border: color-mix(in srgb, rgb(255 255 255 / 15%) 78%, rgb(255 255 255 / 32%));
54
+ --border-hover: color-mix(in srgb, rgb(255 255 255 / 28%) 76%, rgb(255 255 255 / 44%));
55
55
  --code-bg: hsl(var(--accent-hue) 18% 6%);
56
- --glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 10%), inset 0 -1px 0 rgb(0 0 0 / 24%), var(--shadow-soft);
56
+ --glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 13%), inset 0 -1px 0 rgb(0 0 0 / 28%), var(--shadow-soft);
57
57
  --notification-error-bg: rgb(255 123 114 / 13%);
58
58
  --notification-error-fg: #ff7b72;
59
59
  --notification-info-bg: rgb(88 166 255 / 13%);
@@ -62,7 +62,7 @@
62
62
  --notification-success-fg: #56d364;
63
63
  --notification-warning-bg: rgb(210 153 34 / 15%);
64
64
  --notification-warning-fg: #d29922;
65
- --shadow-soft: 0 24px 80px rgb(0 0 0 / 40%);
65
+ --shadow-soft: 0 26px 84px rgb(0 0 0 / 46%);
66
66
  --surface-elevated: #151515;
67
67
  --surface-muted: #202020;
68
68
  --success-bg: hsl(var(--accent-hue) 74% 66% / 12%);
@@ -88,8 +88,10 @@
88
88
  --bg-primary: #050505;
89
89
  --bg-secondary: #0b0b0b;
90
90
  --bg-tertiary: #141414;
91
- --border: color-mix(in srgb, rgb(255 255 255 / 22%) 78%, rgb(255 255 255 / 44%));
92
- --border-hover: color-mix(in srgb, rgb(255 255 255 / 36%) 80%, rgb(255 255 255 / 52%));
91
+ --border: color-mix(in srgb, rgb(255 255 255 / 24%) 80%, rgb(255 255 255 / 44%));
92
+ --border-hover: color-mix(in srgb, rgb(255 255 255 / 38%) 82%, rgb(255 255 255 / 52%));
93
+ --glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 14%), inset 0 -1px 0 rgb(0 0 0 / 30%), var(--shadow-soft);
94
+ --shadow-soft: 0 28px 88px rgb(0 0 0 / 52%);
93
95
  --surface-elevated: #101010;
94
96
  --surface-muted: #181818;
95
97
  --text-muted: rgb(255 255 255 / 58%);
@@ -205,13 +207,36 @@ body {
205
207
  background:
206
208
  radial-gradient(ellipse at top left, color-mix(in srgb, var(--accent) 16%, transparent) 0%, transparent 36%),
207
209
  var(--bg-secondary);
210
+ background-attachment: fixed;
211
+ background-repeat: no-repeat;
208
212
  color: var(--text-primary);
209
213
  font-family: var(--font-family-app);
210
214
  line-height: 1.6;
211
215
  margin: 0;
216
+ min-height: 100vh;
212
217
  font-size: var(--font-size-app);
213
218
  }
214
219
 
220
+ :root[data-theme="dark"] body {
221
+ background:
222
+ radial-gradient(ellipse 78% 64% at top left,
223
+ hsl(var(--accent-hue) 96% 22% / 62%) 0%,
224
+ hsl(var(--accent-hue) 92% 18% / 34%) 34%,
225
+ transparent 58%),
226
+ radial-gradient(ellipse 72% 62% at bottom right,
227
+ hsl(var(--accent-hue) 90% 16% / 38%) 0%,
228
+ hsl(var(--accent-hue) 84% 12% / 20%) 38%,
229
+ transparent 66%),
230
+ radial-gradient(ellipse 66% 54% at bottom left,
231
+ hsl(var(--accent-hue) 90% 15% / 30%) 0%,
232
+ hsl(var(--accent-hue) 80% 11% / 16%) 38%,
233
+ transparent 68%),
234
+ linear-gradient(135deg,
235
+ color-mix(in srgb, hsl(var(--accent-hue) 92% 16%) 44%, var(--bg-secondary)) 0%,
236
+ color-mix(in srgb, hsl(var(--accent-hue) 80% 12%) 14%, var(--bg-secondary)) 48%,
237
+ color-mix(in srgb, var(--accent) 6%, var(--bg-primary)) 100%);
238
+ }
239
+
215
240
  button,
216
241
  input,
217
242
  select {
@@ -1721,7 +1746,7 @@ code {
1721
1746
 
1722
1747
  .hero-panel {
1723
1748
  background:
1724
- linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, transparent), transparent 58%),
1749
+ linear-gradient(135deg, color-mix(in srgb, var(--accent) 11%, transparent), transparent 58%),
1725
1750
  var(--surface-elevated);
1726
1751
  padding: clamp(24px, 5vw, 42px);
1727
1752
  }
@@ -1849,7 +1874,7 @@ code {
1849
1874
 
1850
1875
  .notifications-hero {
1851
1876
  background:
1852
- linear-gradient(135deg, var(--notification-bg, color-mix(in srgb, var(--accent) 12%, transparent)), transparent 62%),
1877
+ linear-gradient(135deg, var(--notification-bg, color-mix(in srgb, var(--accent) 10%, transparent)), transparent 62%),
1853
1878
  var(--surface-elevated);
1854
1879
  }
1855
1880
 
@@ -2228,7 +2253,7 @@ code {
2228
2253
 
2229
2254
  .pulse-hero {
2230
2255
  background:
2231
- linear-gradient(135deg, color-mix(in srgb, var(--accent) 18%, transparent), transparent 64%),
2256
+ linear-gradient(135deg, color-mix(in srgb, var(--accent) 12%, transparent), transparent 64%),
2232
2257
  var(--surface-elevated);
2233
2258
  }
2234
2259
 
@@ -3149,8 +3174,8 @@ button.managed-toggle:focus-visible {
3149
3174
  .recommended-app-card:hover,
3150
3175
  .recommended-app-card:focus-within {
3151
3176
  background:
3152
- linear-gradient(135deg, color-mix(in srgb, var(--accent) 16%, transparent), transparent 46%),
3153
- color-mix(in srgb, var(--accent) 7%, var(--surface-muted));
3177
+ linear-gradient(135deg, color-mix(in srgb, var(--accent) 11%, transparent), transparent 42%),
3178
+ color-mix(in srgb, var(--accent) 5%, var(--surface-muted));
3154
3179
  border-color: var(--border-accent);
3155
3180
  box-shadow: 0 18px 46px color-mix(in srgb, var(--accent) 10%, transparent);
3156
3181
  transform: translateY(-1px);
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.40
15
+ # Version: 3.29.42
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)