aidevops 3.29.37 → 3.29.39

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.37-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.39-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.37
1
+ 3.29.39
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.37
8
+ # Version: 3.29.39
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.37",
3
+ "version": "3.29.39",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as readline from "node:readline";
3
3
  import { Hono } from "hono";
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
+ 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, TAMBO_PROVIDER_CONFIG, TAMBO_PROXY_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
5
5
  import { readFileExplorer } from "./file-adapter";
6
6
  import { readStatus, readVaultStatus } from "./status-adapter";
7
7
 
@@ -53,6 +53,22 @@ export function createGuiApiApp() {
53
53
  return context.json(readVaultStatus());
54
54
  });
55
55
 
56
+ app.get(TAMBO_PROXY_ROUTE_MANIFEST.route, (context) => {
57
+ const tenantRef = context.req.query("tenant_ref") ?? "local";
58
+ const workspaceRef = context.req.query("workspace_ref") ?? "aidevops";
59
+ const sessionRef = context.req.query("session_ref") ?? "conversation:local";
60
+
61
+ return context.json(createEnvelope({
62
+ operation_id: TAMBO_PROXY_ROUTE_MANIFEST.operation_id,
63
+ source: { surface: "conversations", authority: "server-side Tambo proxy metadata", path_refs: ["packages/gui-shared/src/tambo.ts"] },
64
+ data: {
65
+ ...TAMBO_PROVIDER_CONFIG,
66
+ thread_key_ref: `${tenantRef}:${workspaceRef}:${sessionRef}`,
67
+ },
68
+ warnings: ["Tambo provider secrets stay server-side; this route exposes component metadata and scoped thread keys only."],
69
+ }));
70
+ });
71
+
56
72
  app.post(APP_ACTION_ROUTE_MANIFEST.route, (context) => {
57
73
  const appId = context.req.param("appId");
58
74
  const action = context.req.param("action") as GuiAppActionId;
@@ -1,6 +1,6 @@
1
1
  export type GuiRouteClassification = "read" | "write" | "destructive";
2
2
 
3
- export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read" | "apps.action.run" | "apps.action.status";
3
+ export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read" | "apps.action.run" | "apps.action.status" | "tambo.session.read";
4
4
 
5
5
  export type GuiFileRootId = "agents" | "config" | "localSetup" | "git";
6
6
 
@@ -231,6 +231,122 @@ export interface GuiConversationUsageMetadata {
231
231
  cost_ref: string | null;
232
232
  }
233
233
 
234
+ export type GuiPulseWorkerEventType = "pulse_run" | "worker_session" | "command_job" | "ci_check" | "issue" | "pull_request" | "review" | "systemic_fix";
235
+
236
+ export type GuiPulseWorkerStatus = "healthy" | "attention" | "blocked" | "running" | "completed" | "failed" | "deferred";
237
+
238
+ export type GuiPulseWorkerOutcome = "merged" | "closed" | "in_progress" | "needs_maintainer_review" | "blocked" | "failed" | "deferred" | "created_followup";
239
+
240
+ export type GuiPulseWorkerSeverity = "info" | "success" | "warning" | "critical";
241
+
242
+ export type GuiPulseIssueOrigin = "aidevops_created" | "maintainer_created" | "origin_interactive" | "third_party" | "unknown";
243
+
244
+ export type GuiPulseAuthorAssociation = "OWNER" | "MEMBER" | "COLLABORATOR" | "CONTRIBUTOR" | "FIRST_TIME_CONTRIBUTOR" | "NONE" | "UNKNOWN";
245
+
246
+ export type GuiPulseResourceKind = "provider_model" | "github_api" | "ci_capacity" | "queue" | "cpu" | "memory" | "disk" | "network";
247
+
248
+ export type GuiPulseProviderId = GuiAiProviderId | "local" | "unknown";
249
+
250
+ export type GuiPulsePeriodBucket = "day" | "week" | "month" | "year";
251
+
252
+ export interface GuiPulseWorkerKpiCard {
253
+ id: string;
254
+ label: string;
255
+ value: string;
256
+ period_label: string;
257
+ scope_label: string;
258
+ comparison_label: string;
259
+ sample_size?: number;
260
+ status: GuiPulseWorkerStatus;
261
+ detail: string;
262
+ }
263
+
264
+ export interface GuiPulseWorkerUsageSnapshot extends GuiConversationUsageMetadata {
265
+ provider: GuiPulseProviderId;
266
+ model_ref: string | null;
267
+ cached_tokens: number;
268
+ wall_time_ms: number;
269
+ estimated_cost_ref: string | null;
270
+ }
271
+
272
+ export interface GuiPulseResourceSnapshot {
273
+ kind: GuiPulseResourceKind;
274
+ label: string;
275
+ available_label: string;
276
+ pressure: "low" | "medium" | "high" | "unknown";
277
+ observed_at: string;
278
+ reset_at: string | null;
279
+ }
280
+
281
+ export interface GuiPulseWorkerChartPoint {
282
+ period: GuiPulsePeriodBucket;
283
+ period_label: string;
284
+ scope_label: string;
285
+ bucket_start: string;
286
+ bucket_end: string;
287
+ value: number;
288
+ }
289
+
290
+ export interface GuiPulseWorkerChartSeries {
291
+ id: string;
292
+ label: string;
293
+ unit: "percent" | "count" | "tokens" | "cost_ref" | "milliseconds";
294
+ points: GuiPulseWorkerChartPoint[];
295
+ }
296
+
297
+ export interface GuiPulseWorkerActivityEvent {
298
+ id: string;
299
+ type: GuiPulseWorkerEventType;
300
+ status: GuiPulseWorkerStatus;
301
+ outcome: GuiPulseWorkerOutcome;
302
+ severity: GuiPulseWorkerSeverity;
303
+ occurred_at: string;
304
+ title: string;
305
+ summary: string;
306
+ pulse_run_ref: string | null;
307
+ worker_session_ref: string | null;
308
+ issue_ref: string | null;
309
+ pull_request_ref: string | null;
310
+ command_job_ref: string | null;
311
+ repo_ref: string | null;
312
+ actor_ref: string | null;
313
+ issue_origin: GuiPulseIssueOrigin;
314
+ author_association: GuiPulseAuthorAssociation;
315
+ duration_ms: number | null;
316
+ usage: GuiPulseWorkerUsageSnapshot | null;
317
+ resources: GuiPulseResourceSnapshot[];
318
+ drilldown_sections: Array<{ label: string; body: string }>;
319
+ }
320
+
321
+ export interface GuiPulseWorkerFilterOption {
322
+ id: string;
323
+ label: string;
324
+ count: number;
325
+ }
326
+
327
+ export interface GuiPulseWorkerSummary {
328
+ value_policy: "metadata_only_no_prompt_payloads_no_secrets";
329
+ selected_period: GuiPulsePeriodBucket;
330
+ period_label: string;
331
+ scope_label: string;
332
+ comparison_label: string;
333
+ updated_at: string;
334
+ kpis: GuiPulseWorkerKpiCard[];
335
+ attention: Array<{ id: string; severity: GuiPulseWorkerSeverity; title: string; detail: string; event_ref: string | null }>;
336
+ filters: {
337
+ repos: GuiPulseWorkerFilterOption[];
338
+ event_types: GuiPulseWorkerFilterOption[];
339
+ outcomes: GuiPulseWorkerFilterOption[];
340
+ resources: GuiPulseWorkerFilterOption[];
341
+ providers: GuiPulseWorkerFilterOption[];
342
+ issue_origins: GuiPulseWorkerFilterOption[];
343
+ authors: GuiPulseWorkerFilterOption[];
344
+ author_associations: GuiPulseWorkerFilterOption[];
345
+ };
346
+ charts: GuiPulseWorkerChartSeries[];
347
+ events: GuiPulseWorkerActivityEvent[];
348
+ }
349
+
234
350
  export interface GuiConversationMessage {
235
351
  id: string;
236
352
  conversation_id: string;
@@ -478,6 +594,7 @@ export interface GuiStatusData {
478
594
  managed_apps: GuiManagedAppSummary[];
479
595
  notifications: GuiNotificationSummary[];
480
596
  vault: GuiVaultStatusData;
597
+ pulse_workers: GuiPulseWorkerSummary;
481
598
  capabilities: GuiCapabilitySummary[];
482
599
  secrets: GuiSecretReference[];
483
600
  placeholders: string[];
@@ -1,4 +1,124 @@
1
- import { GUI_FILE_ROOTS, type GuiFileExplorerData, type GuiStatusData } from "./contracts";
1
+ import { GUI_FILE_ROOTS, type GuiFileExplorerData, type GuiPulseWorkerSummary, type GuiStatusData } from "./contracts";
2
+
3
+ const pulseScope = "all managed repos";
4
+ const pulsePeriod = "Last 24h";
5
+ const pulseChartBuckets = {
6
+ day: { period_label: pulsePeriod, bucket_start: "2026-06-30T00:00:00.000Z", bucket_end: "2026-06-30T23:59:59.000Z", value: 86 },
7
+ week: { period_label: "This week", bucket_start: "2026-06-24T00:00:00.000Z", bucket_end: "2026-06-30T23:59:59.000Z", value: 82 },
8
+ month: { period_label: "This month", bucket_start: "2026-06-01T00:00:00.000Z", bucket_end: "2026-06-30T23:59:59.000Z", value: 82 },
9
+ year: { period_label: "This year", bucket_start: "2026-01-01T00:00:00.000Z", bucket_end: "2026-12-31T23:59:59.000Z", value: 82 },
10
+ } as const;
11
+
12
+ export const pulseWorkersFixture: GuiPulseWorkerSummary = {
13
+ value_policy: "metadata_only_no_prompt_payloads_no_secrets",
14
+ selected_period: "day",
15
+ period_label: pulsePeriod,
16
+ scope_label: pulseScope,
17
+ comparison_label: "Previous 24h baseline",
18
+ updated_at: "2026-06-30T09:45:00.000Z",
19
+ kpis: [
20
+ { id: "healthy-sessions", label: "Healthy worker sessions", value: "86%", period_label: pulsePeriod, scope_label: pulseScope, comparison_label: "up 8 points vs previous 24h", sample_size: 21, status: "healthy", detail: "18 of 21 sessions ended merged, closed, or intentionally deferred." },
21
+ { id: "attention-queue", label: "Attention queue", value: "5", period_label: pulsePeriod, scope_label: pulseScope, comparison_label: "down 2 vs previous 24h", sample_size: 184, status: "attention", detail: "Review stalls, API pressure, and CI capacity are grouped before raw events." },
22
+ { id: "token-cost", label: "Token and cost sample", value: "1.8M / $14", period_label: pulsePeriod, scope_label: "provider scope", comparison_label: "estimated, metadata-only", sample_size: 11, status: "healthy", detail: "Provider/model totals exclude prompts, terminal payloads, secrets, and credential paths." },
23
+ { id: "systemic-fixes", label: "Systemic fixes created", value: "9", period_label: "Trailing 7d", scope_label: "aidevops repo", comparison_label: "up 3 vs prior week", sample_size: 9, status: "completed", detail: "Blindspots become worker-ready follow-up tasks when patterns repeat." },
24
+ ],
25
+ attention: [
26
+ { id: "terminal-ci", severity: "warning", title: "2 PRs waiting on terminal CI results", detail: "Pending checks are tracked separately from failures to avoid noisy repair loops.", event_ref: "event:review-gate-25909" },
27
+ { id: "stale-loop", severity: "warning", title: "1 worker loop matched stale-symptom diagnostics", detail: "Pulse should suggest a systemic diagnostics task instead of another retry.", event_ref: null },
28
+ { id: "github-api", severity: "critical", title: "GitHub API allowance below dispatch comfort threshold", detail: "REST budget remains available; GraphQL-heavy list operations should pause.", event_ref: "event:community-25876" },
29
+ ],
30
+ filters: {
31
+ repos: [{ id: "repo:marcusquinn/aidevops", label: "marcusquinn/aidevops", count: 184 }],
32
+ event_types: [{ id: "worker_session", label: "Worker session", count: 21 }, { id: "review", label: "Review gate", count: 17 }, { id: "issue", label: "Issue", count: 12 }],
33
+ outcomes: [{ id: "merged", label: "Merged", count: 12 }, { id: "in_progress", label: "In progress", count: 4 }, { id: "needs_maintainer_review", label: "Needs maintainer review", count: 2 }],
34
+ resources: [{ id: "provider_model", label: "Provider / model", count: 21 }, { id: "github_api", label: "GitHub API", count: 8 }, { id: "ci_capacity", label: "CI capacity", count: 7 }],
35
+ providers: [{ id: "openai:gpt-5.5", label: "OpenAI · gpt-5.5", count: 11 }, { id: "anthropic:sonnet", label: "Anthropic · sonnet", count: 7 }],
36
+ issue_origins: [{ id: "origin_interactive", label: "Interactive origin", count: 8 }, { id: "aidevops_created", label: "aidevops-created", count: 9 }, { id: "third_party", label: "Third-party/community", count: 3 }],
37
+ authors: [{ id: "actor:maintainer", label: "Maintainer", count: 17 }, { id: "actor:community", label: "Community reporter", count: 3 }],
38
+ author_associations: [{ id: "OWNER", label: "OWNER", count: 9 }, { id: "MEMBER", label: "MEMBER", count: 7 }, { id: "CONTRIBUTOR", label: "CONTRIBUTOR", count: 3 }],
39
+ },
40
+ charts: (["day", "week", "month", "year"] as const).map((period) => {
41
+ const bucket = pulseChartBuckets[period];
42
+
43
+ return {
44
+ id: `health-${period}`,
45
+ label: `Health trend · ${period}`,
46
+ unit: "percent",
47
+ points: [{ period, period_label: bucket.period_label, scope_label: pulseScope, bucket_start: bucket.bucket_start, bucket_end: bucket.bucket_end, value: bucket.value }],
48
+ };
49
+ }),
50
+ events: [
51
+ {
52
+ id: "event:worker-25912",
53
+ type: "worker_session",
54
+ status: "running",
55
+ outcome: "in_progress",
56
+ severity: "info",
57
+ occurred_at: "2026-06-30T09:42:00.000Z",
58
+ title: "Worker session",
59
+ summary: "Implementing a GUI observability child task with read-only metadata fixtures.",
60
+ pulse_run_ref: "pulse:2026-06-30-am",
61
+ worker_session_ref: "worker:25912:gpt-5.5",
62
+ issue_ref: "#25912",
63
+ pull_request_ref: null,
64
+ command_job_ref: null,
65
+ repo_ref: "marcusquinn/aidevops",
66
+ actor_ref: "worker:auto-dispatch",
67
+ issue_origin: "origin_interactive",
68
+ author_association: "MEMBER",
69
+ duration_ms: 1420000,
70
+ usage: { provider: "openai", provider_ref: "provider:openai", model_ref: "model:gpt-5.5", input_tokens: 96000, output_tokens: 18000, cached_tokens: 42000, total_tokens: 114000, cost_ref: "cost:estimated-usd-0.88", estimated_cost_ref: "$0.88 estimated", wall_time_ms: 1420000 },
71
+ resources: [{ kind: "github_api", label: "REST API", available_label: "3,920 remaining", pressure: "low", observed_at: "2026-06-30T09:42:00.000Z", reset_at: "2026-06-30T10:00:00.000Z" }],
72
+ drilldown_sections: [{ label: "Why", body: "Parent task requested high-density Pulse & Workers observability without raw prompt payloads." }, { label: "Systemic fix", body: "Create follow-up only when repeated blindspots appear across workers." }],
73
+ },
74
+ {
75
+ id: "event:review-gate-25909",
76
+ type: "review",
77
+ status: "completed",
78
+ outcome: "merged",
79
+ severity: "success",
80
+ occurred_at: "2026-06-30T09:18:00.000Z",
81
+ title: "Review gate",
82
+ summary: "PR merged after terminal checks completed successfully.",
83
+ pulse_run_ref: "pulse:2026-06-30-am",
84
+ worker_session_ref: "worker:25909:sonnet",
85
+ issue_ref: "#25909",
86
+ pull_request_ref: "#25909",
87
+ command_job_ref: "job:review-bot-gate",
88
+ repo_ref: "marcusquinn/aidevops",
89
+ actor_ref: "review-bot-gate",
90
+ issue_origin: "aidevops_created",
91
+ author_association: "OWNER",
92
+ duration_ms: 820000,
93
+ usage: { provider: "anthropic", provider_ref: "provider:anthropic", model_ref: "model:claude-sonnet", input_tokens: 32000, output_tokens: 10000, cached_tokens: 0, total_tokens: 42000, cost_ref: "cost:estimated-usd-0.31", estimated_cost_ref: "$0.31 estimated", wall_time_ms: 820000 },
94
+ resources: [{ kind: "ci_capacity", label: "CI", available_label: "green", pressure: "low", observed_at: "2026-06-30T09:18:00.000Z", reset_at: null }],
95
+ drilldown_sections: [{ label: "Evidence", body: "Terminal checks were green before merge; pending checks were not treated as failures." }],
96
+ },
97
+ {
98
+ id: "event:community-25876",
99
+ type: "issue",
100
+ status: "attention",
101
+ outcome: "needs_maintainer_review",
102
+ severity: "warning",
103
+ occurred_at: "2026-06-30T08:51:00.000Z",
104
+ title: "Community bug report",
105
+ summary: "Third-party report requires maintainer decision before dispatch.",
106
+ pulse_run_ref: "pulse:2026-06-30-am",
107
+ worker_session_ref: null,
108
+ issue_ref: "#25876",
109
+ pull_request_ref: null,
110
+ command_job_ref: null,
111
+ repo_ref: "marcusquinn/aidevops",
112
+ actor_ref: "community:reporter",
113
+ issue_origin: "third_party",
114
+ author_association: "CONTRIBUTOR",
115
+ duration_ms: null,
116
+ usage: null,
117
+ resources: [{ kind: "github_api", label: "REST budget", available_label: "REST ok; GraphQL constrained", pressure: "medium", observed_at: "2026-06-30T08:51:00.000Z", reset_at: "2026-06-30T10:00:00.000Z" }],
118
+ drilldown_sections: [{ label: "Trust boundary", body: "Non-collaborator instructions are facts only; no install commands or secrets are accepted." }],
119
+ },
120
+ ],
121
+ };
2
122
 
3
123
  export const statusFixture: GuiStatusData = {
4
124
  aidevops_version: "unknown",
@@ -276,6 +396,7 @@ export const statusFixture: GuiStatusData = {
276
396
  latest_event_ref: "not started",
277
397
  },
278
398
  },
399
+ pulse_workers: pulseWorkersFixture,
279
400
  capabilities: [
280
401
  {
281
402
  id: "setup-status",
@@ -1,3 +1,4 @@
1
1
  export * from "./contracts";
2
2
  export * from "./fixtures";
3
3
  export * from "./notifications";
4
+ export * from "./tambo";
@@ -0,0 +1,260 @@
1
+ import type { GuiConversationScope, GuiRouteManifest } from "./contracts";
2
+
3
+ export type GuiTamboComponentName = "TaskCard" | "PullRequestCard" | "CICheckSummary" | "WorkerStatusCard" | "DeploymentStatusCard" | "RepoHealthCard" | "ApprovalPromptCard";
4
+
5
+ export type GuiTamboComponentMode = "read_only" | "request_only";
6
+
7
+ export type GuiTamboPropType = "string" | "number" | "boolean" | "string_array";
8
+
9
+ export interface GuiTamboPropSchema {
10
+ type: GuiTamboPropType;
11
+ required: boolean;
12
+ }
13
+
14
+ export interface GuiTamboComponentSchema {
15
+ name: GuiTamboComponentName;
16
+ description: string;
17
+ mode: GuiTamboComponentMode;
18
+ additionalProperties: false;
19
+ properties: Record<string, GuiTamboPropSchema>;
20
+ }
21
+
22
+ export interface GuiTamboComponentPayload {
23
+ component: GuiTamboComponentName;
24
+ tenant_ref: string;
25
+ session_ref: string;
26
+ read_only: boolean;
27
+ props: Record<string, unknown>;
28
+ }
29
+
30
+ export interface GuiTamboValidationResult {
31
+ ok: boolean;
32
+ component: GuiTamboComponentName | null;
33
+ props: Record<string, unknown>;
34
+ errors: string[];
35
+ }
36
+
37
+ export interface GuiTamboProviderConfig {
38
+ provider: "tambo";
39
+ proxy_route: string;
40
+ secret_policy: "server_only_no_browser_tokens";
41
+ thread_key_policy: "tenant_workspace_session_scoped";
42
+ components: GuiTamboComponentSchema[];
43
+ deferred_interactables: string[];
44
+ }
45
+
46
+ export const TAMBO_PROXY_ROUTE_MANIFEST: GuiRouteManifest = {
47
+ route: "/api/tambo/session",
48
+ method: "GET",
49
+ operation_id: "tambo.session.read",
50
+ classification: "read",
51
+ source_surface: "conversations",
52
+ adapter: "tambo.readProviderConfig",
53
+ command_pattern: ["server-proxied", "metadata-only", "no-browser-secrets"],
54
+ redactions: ["secret_values", "credential_paths", "private_key_material", "tambo_api_keys"],
55
+ };
56
+
57
+ export const TAMBO_COMPONENT_SCHEMAS: readonly GuiTamboComponentSchema[] = [
58
+ {
59
+ name: "TaskCard",
60
+ description: "Read-only task or issue status card.",
61
+ mode: "read_only",
62
+ additionalProperties: false,
63
+ properties: {
64
+ title: { type: "string", required: true },
65
+ status: { type: "string", required: true },
66
+ priority: { type: "string", required: false },
67
+ owner: { type: "string", required: false },
68
+ repo: { type: "string", required: false },
69
+ reference: { type: "string", required: false },
70
+ },
71
+ },
72
+ {
73
+ name: "PullRequestCard",
74
+ description: "Read-only pull request summary card.",
75
+ mode: "read_only",
76
+ additionalProperties: false,
77
+ properties: {
78
+ title: { type: "string", required: true },
79
+ status: { type: "string", required: true },
80
+ branch: { type: "string", required: false },
81
+ checks: { type: "string", required: false },
82
+ review: { type: "string", required: false },
83
+ reference: { type: "string", required: false },
84
+ },
85
+ },
86
+ {
87
+ name: "CICheckSummary",
88
+ description: "Read-only CI status summary card.",
89
+ mode: "read_only",
90
+ additionalProperties: false,
91
+ properties: {
92
+ status: { type: "string", required: true },
93
+ passed: { type: "number", required: true },
94
+ failed: { type: "number", required: true },
95
+ pending: { type: "number", required: false },
96
+ summary: { type: "string", required: false },
97
+ },
98
+ },
99
+ {
100
+ name: "WorkerStatusCard",
101
+ description: "Read-only worker progress card.",
102
+ mode: "read_only",
103
+ additionalProperties: false,
104
+ properties: {
105
+ worker: { type: "string", required: true },
106
+ status: { type: "string", required: true },
107
+ task: { type: "string", required: false },
108
+ progress: { type: "number", required: false },
109
+ blockers: { type: "string_array", required: false },
110
+ },
111
+ },
112
+ {
113
+ name: "DeploymentStatusCard",
114
+ description: "Read-only deployment environment status card.",
115
+ mode: "read_only",
116
+ additionalProperties: false,
117
+ properties: {
118
+ environment: { type: "string", required: true },
119
+ status: { type: "string", required: true },
120
+ version: { type: "string", required: false },
121
+ health: { type: "string", required: false },
122
+ },
123
+ },
124
+ {
125
+ name: "RepoHealthCard",
126
+ description: "Read-only repository health card.",
127
+ mode: "read_only",
128
+ additionalProperties: false,
129
+ properties: {
130
+ repo: { type: "string", required: true },
131
+ status: { type: "string", required: true },
132
+ open_prs: { type: "number", required: false },
133
+ failing_checks: { type: "number", required: false },
134
+ notes: { type: "string_array", required: false },
135
+ },
136
+ },
137
+ {
138
+ name: "ApprovalPromptCard",
139
+ description: "Request-only approval card. Mutating approval execution is deferred until audited approval tooling exists.",
140
+ mode: "request_only",
141
+ additionalProperties: false,
142
+ properties: {
143
+ action: { type: "string", required: true },
144
+ reason: { type: "string", required: true },
145
+ risk: { type: "string", required: true },
146
+ requested_by: { type: "string", required: false },
147
+ disabled: { type: "boolean", required: true },
148
+ },
149
+ },
150
+ ] as const;
151
+
152
+ export const TAMBO_PROVIDER_CONFIG: GuiTamboProviderConfig = {
153
+ provider: "tambo",
154
+ proxy_route: TAMBO_PROXY_ROUTE_MANIFEST.route,
155
+ secret_policy: "server_only_no_browser_tokens",
156
+ thread_key_policy: "tenant_workspace_session_scoped",
157
+ components: [...TAMBO_COMPONENT_SCHEMAS],
158
+ deferred_interactables: ["mutating approvals", "deployment actions", "PR merge/retry actions", "worker dispatch writes"],
159
+ };
160
+
161
+ export function validateTamboComponentPayload(payload: unknown, scope: GuiConversationScope): GuiTamboValidationResult {
162
+ const envelope = validateTamboPayloadEnvelope(payload, scope);
163
+ if (!envelope.ok) {
164
+ return envelope.result;
165
+ }
166
+
167
+ const errors = validateTamboComponentProps(envelope.schema, envelope.props);
168
+ return { ok: errors.length === 0, component: envelope.schema.name, props: envelope.props, errors };
169
+ }
170
+
171
+ type TamboPayloadEnvelopeValidation =
172
+ | { ok: true; schema: GuiTamboComponentSchema; props: Record<string, unknown> }
173
+ | { ok: false; result: GuiTamboValidationResult };
174
+
175
+ function validateTamboPayloadEnvelope(payload: unknown, scope: GuiConversationScope): TamboPayloadEnvelopeValidation {
176
+ if (!isRecord(payload)) {
177
+ return { ok: false, result: invalid(null, {}, "payload_not_object") };
178
+ }
179
+ if (typeof payload.component !== "string") {
180
+ return { ok: false, result: invalid(null, {}, "component_missing") };
181
+ }
182
+
183
+ const schema = findTamboComponentSchema(payload.component);
184
+ if (schema === undefined) {
185
+ return { ok: false, result: invalid(null, {}, "component_not_registered") };
186
+ }
187
+
188
+ const envelopeError = validateScopedTamboEnvelope(payload, scope);
189
+ if (envelopeError !== null) {
190
+ return { ok: false, result: invalid(schema.name, {}, envelopeError) };
191
+ }
192
+ if (!isRecord(payload.props)) {
193
+ return { ok: false, result: invalid(schema.name, {}, "props_not_object") };
194
+ }
195
+
196
+ return { ok: true, schema, props: payload.props };
197
+ }
198
+
199
+ function findTamboComponentSchema(component: string): GuiTamboComponentSchema | undefined {
200
+ return TAMBO_COMPONENT_SCHEMAS.find((candidate) => candidate.name === component);
201
+ }
202
+
203
+ function validateScopedTamboEnvelope(payload: Record<string, unknown>, scope: GuiConversationScope): string | null {
204
+ if (payload.tenant_ref !== scope.tenant_ref) {
205
+ return "tenant_scope_mismatch";
206
+ }
207
+ if (typeof payload.session_ref !== "string" || payload.session_ref.length === 0) {
208
+ return "session_ref_missing";
209
+ }
210
+ if (payload.read_only !== true) {
211
+ return "component_not_read_only";
212
+ }
213
+ return null;
214
+ }
215
+
216
+ function validateTamboComponentProps(schema: GuiTamboComponentSchema, props: Record<string, unknown>): string[] {
217
+ return [
218
+ ...findUnexpectedTamboProps(schema, props),
219
+ ...findInvalidTamboProps(schema, props),
220
+ ...findTamboComponentSpecificErrors(schema, props),
221
+ ];
222
+ }
223
+
224
+ function findUnexpectedTamboProps(schema: GuiTamboComponentSchema, props: Record<string, unknown>): string[] {
225
+ return Object.keys(props)
226
+ .filter((key) => schema.properties[key] === undefined)
227
+ .map((key) => `unexpected_prop:${key}`);
228
+ }
229
+
230
+ function findInvalidTamboProps(schema: GuiTamboComponentSchema, props: Record<string, unknown>): string[] {
231
+ return Object.entries(schema.properties).flatMap(([key, propSchema]) => {
232
+ const value = props[key];
233
+ if (value === undefined) {
234
+ return propSchema.required ? [`missing_prop:${key}`] : [];
235
+ }
236
+ return valueMatchesType(value, propSchema.type) ? [] : [`invalid_prop:${key}`];
237
+ });
238
+ }
239
+
240
+ function findTamboComponentSpecificErrors(schema: GuiTamboComponentSchema, props: Record<string, unknown>): string[] {
241
+ if (schema.name === "ApprovalPromptCard" && props.disabled !== true) {
242
+ return ["approval_prompt_must_be_disabled"];
243
+ }
244
+ return [];
245
+ }
246
+
247
+ function invalid(component: GuiTamboComponentName | null, props: Record<string, unknown>, error: string): GuiTamboValidationResult {
248
+ return { ok: false, component, props, errors: [error] };
249
+ }
250
+
251
+ function isRecord(value: unknown): value is Record<string, unknown> {
252
+ return typeof value === "object" && value !== null && !Array.isArray(value);
253
+ }
254
+
255
+ function valueMatchesType(value: unknown, type: GuiTamboPropType): boolean {
256
+ if (type === "string_array") {
257
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
258
+ }
259
+ return typeof value === type;
260
+ }
@@ -3,6 +3,7 @@ import type { GuiMachineSummary, GuiStatusData } from "@aidevops/gui-shared";
3
3
  import { useEffect, useState, type ReactNode } from "react";
4
4
  import type { IconType } from "react-icons";
5
5
  import {
6
+ FiActivity,
6
7
  FiBookmark,
7
8
  FiBox,
8
9
  FiBriefcase,
@@ -41,6 +42,7 @@ import { VaultPadlock, vaultCollectionForSurface } from "./VaultBadges";
41
42
  export { hueFromInputValue } from "./AppearanceControls";
42
43
 
43
44
  const surfaceIcons: Record<SurfaceIconName, IconType> = {
45
+ activity: FiActivity,
44
46
  apps: FiBox,
45
47
  bookmark: FiBookmark,
46
48
  brand: FiBriefcase,
@@ -8,6 +8,7 @@ import { inventorySurfaceConfigs, surfaceIds, text } from "./app-model";
8
8
  import { CommandPalette, type CommandPaletteSelection, commandPaletteShortcutQuery } from "./CommandPalette";
9
9
  import { CommsConversationSurface } from "./CommsConversationSurface";
10
10
  import { FileExplorerSurface } from "./FileExplorerSurface";
11
+ import { TamboConversationPart } from "./GenUiCards";
11
12
  import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
12
13
  import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
13
14
  import { isVaultSurfaceLocked, vaultCollectionForSurface } from "./VaultBadges";
@@ -289,6 +290,10 @@ function AiSessionsSurface({ selectedRepoIndex, selectedSessionId, status }: { s
289
290
  <AttachmentCard label="Context attachment" detail={selectedRepo ? `Selected repo context: ${selectedRepo.path_ref}` : "No local repos were discovered yet."} />
290
291
  <MessageMarker label="Reasoning" detail="Reasoning disclosure, tool status, sources, retries, errors, and token usage render here when the AI transport supplies those parts." />
291
292
  <ToolStatusCard />
293
+ <TamboConversationPart
294
+ part={{ id: "ai-session-tambo-preview", message_id: "ai-session-preview", kind: "tambo_component", ordinal: 1, text: null, payload_json: { component: "RepoHealthCard", tenant_ref: "local", session_ref: selectedSession?.id_ref ?? "ai-session-preview", read_only: true, props: { repo: selectedRepo?.name ?? "local workspace", status: "ready for Tambo cards", open_prs: 0, failing_checks: 0, notes: ["Server-proxied provider metadata", "Read-only schemas first"] } }, file_ref: null, source_ref: "tambo:ai-session-preview" }}
295
+ scope={{ tenant_ref: "local", workspace_ref: "aidevops", repo_ref: selectedRepo?.slug ?? null }}
296
+ />
292
297
  </div>
293
298
  <form className="chat-composer ai-composer" aria-label="AI prompt composer" data-tour="ai-composer">
294
299
  <button disabled title="Attachments need the audited upload/context adapter" type="button"><FiPaperclip aria-hidden="true" /> Attach</button>
@@ -380,7 +385,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
380
385
  aiSessions: <AiSessionsSurface selectedRepoIndex={0} status={status} />,
381
386
  channels: <CommsConversationSurface mode="channels" />,
382
387
  directMessages: <CommsConversationSurface mode="directMessages" />,
383
- workers: <PlannedSurface label={text.workers} detail={text.workersIntro} />,
388
+ workers: <PulseWorkersSurface status={status} />,
384
389
  repos: <PlannedSurface label={text.repos} detail={text.reposIntro} />,
385
390
  deployments: <PlannedSurface label={text.deployments} detail={text.deploymentsIntro} />,
386
391
  routines: <PlannedSurface label={text.routines} detail={text.routineDetail} />,
@@ -432,6 +437,114 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
432
437
  return staticSurfaces[activeSurface] ?? null;
433
438
  }
434
439
 
440
+ const pulseFilters = ["Repo", "Event type", "Outcome", "Resource", "Provider / model", "Issue origin", "Author", "Author association"] as const;
441
+
442
+ function PulseWorkersSurface({ status }: { status: GuiStatusData }): ReactElement {
443
+ const pulse = status.pulse_workers;
444
+
445
+ return (
446
+ <section className="pulse-workers-surface" aria-label={text.workers}>
447
+ <div className="planned-card pulse-hero">
448
+ <p className="eyebrow">Observability shell · fixture-backed · read-only</p>
449
+ <h2>{text.workers}</h2>
450
+ <p>{text.workersIntro}</p>
451
+ <section className="pulse-scope-strip" aria-label="Pulse scope and time range">
452
+ <span><strong>Period</strong> {pulse.period_label} · {pulse.scope_label}</span>
453
+ <span><strong>Comparison</strong> {pulse.comparison_label}</span>
454
+ <span><strong>Sample</strong> {pulse.events.length} canonical events · {pulse.kpis.reduce((total, kpi) => total + (kpi.sample_size ?? 0), 0)} samples</span>
455
+ <span><strong>Trust boundary</strong> Metadata/status only; protected payloads excluded</span>
456
+ </section>
457
+ </div>
458
+ <section className="pulse-kpi-grid" aria-label="Pulse health summary">
459
+ {pulse.kpis.map((kpi) => (
460
+ <article className="metric-card pulse-kpi-card" key={kpi.id}>
461
+ <span>{kpi.label} · {kpi.period_label} · {kpi.scope_label}</span>
462
+ <strong>{kpi.value}</strong>
463
+ <p>{kpi.detail} {kpi.comparison_label}</p>
464
+ </article>
465
+ ))}
466
+ </section>
467
+ <section className="pulse-layout" aria-label="Pulse observability hierarchy">
468
+ <article className="planned-card pulse-attention-panel">
469
+ <div className="split-heading">
470
+ <div>
471
+ <p className="eyebrow">Exceptions first</p>
472
+ <h3>Needs attention</h3>
473
+ </div>
474
+ <span className="count-pill">planned actions disabled</span>
475
+ </div>
476
+ <ul>
477
+ {pulse.attention.map((item) => <li key={item.id}>{item.title}: {item.detail}</li>)}
478
+ </ul>
479
+ <button disabled title="Action routes need audited worker control APIs" type="button">Create systemic fix (planned)</button>
480
+ </article>
481
+ <article className="planned-card pulse-chart-panel">
482
+ <p className="eyebrow">Trends</p>
483
+ <h3>Health trend placeholder</h3>
484
+ <div className="pulse-chart-placeholder" aria-label="Chart placeholder showing health, queue, token, cost, API, and CI capacity trends" role="img">
485
+ <span>health</span><span>queue</span><span>tokens</span><span>cost</span><span>api</span><span>ci</span>
486
+ </div>
487
+ <p>Charts derive from {pulse.charts.map((chart) => chart.points[0]?.period).filter(Boolean).join("/")} fixture buckets across the canonical event stream: Pulse, workers, commands, CI, issues, PRs, reviews, and outcomes.</p>
488
+ </article>
489
+ </section>
490
+ <section className="planned-card pulse-activity-panel" aria-label="Unified activity stream">
491
+ <div className="split-heading">
492
+ <div>
493
+ <p className="eyebrow">Canonical stream</p>
494
+ <h3>Unified activity</h3>
495
+ </div>
496
+ <section className="pulse-filter-row" aria-label="Quick filters">
497
+ {pulseFilters.map((filter) => <button disabled key={filter} title={`${filter} filter is planned`} type="button">{filter}</button>)}
498
+ </section>
499
+ </div>
500
+ <table className="pulse-activity-table" aria-label="Pulse and worker events">
501
+ <thead>
502
+ <tr className="pulse-activity-row pulse-activity-header">
503
+ <th scope="col">When</th><th scope="col">Event</th><th scope="col">Scope</th><th scope="col">Outcome</th><th scope="col">Resource</th><th scope="col">Origin / actor</th>
504
+ </tr>
505
+ </thead>
506
+ <tbody>
507
+ {pulse.events.map((row) => (
508
+ <tr className="pulse-activity-row" key={row.id}>
509
+ <td data-label="When">{row.occurred_at.slice(11, 16)}</td>
510
+ <td data-label="Event">{row.title}</td>
511
+ <td data-label="Scope">{[row.repo_ref, row.issue_ref ?? row.pull_request_ref].filter(Boolean).join(" ")}</td>
512
+ <td data-label="Outcome">{row.outcome.replaceAll("_", " ")}</td>
513
+ <td data-label="Resource">{resourceSummary(row)}</td>
514
+ <td data-label="Origin / actor">{row.issue_origin.replaceAll("_", "-")} · {row.author_association}</td>
515
+ </tr>
516
+ ))}
517
+ </tbody>
518
+ </table>
519
+ <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>
520
+ </section>
521
+ <section className="pulse-layout" aria-label="Drilldown and planned actions">
522
+ <article className="planned-card pulse-drilldown-panel">
523
+ <p className="eyebrow">Drilldown placeholder</p>
524
+ <h3>What happened, when, why, how, who acted, and what resources were available</h3>
525
+ <p>Selected event details will connect timeline evidence, issue/PR/review context, provider/model/tokens/time/cost metadata, GitHub/API allowance, queue/concurrency, CI capacity, and local resource snapshots without exposing secrets.</p>
526
+ </article>
527
+ <article className="planned-card pulse-actions-panel">
528
+ <p className="eyebrow">Planned controls</p>
529
+ <h3>Actions stay disabled until audited routes land</h3>
530
+ <button disabled title="Terminal output needs a read-only command-output adapter" type="button">Open terminal output (planned)</button>
531
+ <button disabled title="Dispatch needs worker control and trust-boundary APIs" type="button">Redispatch worker (planned)</button>
532
+ <button disabled title="Persistence needs a write-action manifest and audit trail" type="button">Save systemic fix (planned)</button>
533
+ </article>
534
+ </section>
535
+ </section>
536
+ );
537
+ }
538
+
539
+ function resourceSummary(row: GuiStatusData["pulse_workers"]["events"][number]): string {
540
+ const model = row.usage?.model_ref?.replace("model:", "");
541
+ const providerLabel = row.usage?.provider === "openai" ? "OpenAI" : row.usage?.provider === "anthropic" ? "Anthropic" : row.usage?.provider;
542
+ const provider = row.usage === null ? row.resources[0]?.available_label ?? "metadata only" : `${providerLabel ?? "Provider"} · ${model ?? "model metadata pending"}`;
543
+ const tokens = row.usage === null ? "" : ` · ${row.usage.total_tokens.toLocaleString()} tokens`;
544
+
545
+ return `${provider}${tokens}`;
546
+ }
547
+
435
548
  function HelpSurface(): ReactElement {
436
549
  const shortcuts = [
437
550
  ["#", "Channels"],
@@ -1,5 +1,6 @@
1
1
  import { FiAtSign, FiHash, FiMessageSquare, FiSearch } from "react-icons/fi";
2
2
  import { sortConversationMessageParts, sortConversationMessages, type GuiConversationMessage, type GuiConversationMessagePart, type GuiConversationThread } from "../../gui-shared/src";
3
+ import { TamboConversationPart } from "./GenUiCards";
3
4
  import { text } from "./app-model";
4
5
  import { conversationThreads } from "./conversation-fixtures";
5
6
 
@@ -91,13 +92,20 @@ function ConversationMessageRow({ message, parts, thread }: { message: GuiConver
91
92
  return (
92
93
  <article className={`chat-bubble ${speaker}`} data-sender-kind={message.sender_kind}>
93
94
  <strong>{sender?.display_name ?? message.sender_kind}</strong>
94
- {parts.map((part) => <p key={part.id}>{part.text ?? part.kind}</p>)}
95
+ {parts.map((part) => <ConversationPart key={part.id} part={part} thread={thread} />)}
95
96
  <small>{message.status} · {message.created_at}</small>
96
97
  {reactions.length > 0 ? <div className="reaction-row">{reactions.map((reaction) => <span key={reaction.id}>{reaction.reaction}</span>)}</div> : null}
97
98
  </article>
98
99
  );
99
100
  }
100
101
 
102
+ function ConversationPart({ part, thread }: { part: GuiConversationMessagePart; thread: GuiConversationThread }) {
103
+ if (part.kind === "tambo_component" || part.kind === "approval_prompt") {
104
+ return <TamboConversationPart part={part} scope={thread.conversation.scope} />;
105
+ }
106
+ return <p>{part.text ?? part.kind}</p>;
107
+ }
108
+
101
109
  function participantSummary(thread: GuiConversationThread): string {
102
110
  const active = conversationParticipants(thread).filter((participant) => participant.membership_state === "active").length;
103
111
  return `${active} members`;
@@ -0,0 +1,52 @@
1
+ import { validateTamboComponentPayload, type GuiConversationMessagePart, type GuiConversationScope, type GuiTamboValidationResult } from "../../gui-shared/src";
2
+
3
+ export function TamboConversationPart({ part, scope }: { part: GuiConversationMessagePart; scope: GuiConversationScope }) {
4
+ const validation = validateTamboComponentPayload(part.payload_json, scope);
5
+
6
+ if (!validation.ok) {
7
+ return <div className="genui-card genui-card-invalid" data-genui-component={validation.component ?? "unknown"} role="note"><strong>Unsupported DevOps card</strong><small>{validation.errors.join(", ")}</small></div>;
8
+ }
9
+
10
+ return <DevOpsCard validation={validation} />;
11
+ }
12
+
13
+ function DevOpsCard({ validation }: { validation: GuiTamboValidationResult }) {
14
+ const props = validation.props;
15
+ const entries = Object.entries(props).filter(([key]) => key !== "title" && key !== "disabled");
16
+
17
+ return (
18
+ <section className="genui-card" data-genui-component={validation.component ?? "unknown"} aria-label={`${validation.component} DevOps card`}>
19
+ <header>
20
+ <p className="eyebrow">Tambo GenUI · read-only</p>
21
+ <h3>{stringProp(props.title) ?? validation.component}</h3>
22
+ </header>
23
+ <dl>
24
+ {entries.map(([key, value]) => <CardProperty key={key} name={key} value={value} />)}
25
+ </dl>
26
+ {validation.component === "ApprovalPromptCard" ? <p className="genui-deferred">Approval execution is deferred until audited approval tooling exists.</p> : null}
27
+ </section>
28
+ );
29
+ }
30
+
31
+ function CardProperty({ name, value }: { name: string; value: unknown }) {
32
+ return (
33
+ <div>
34
+ <dt>{name.replaceAll("_", " ")}</dt>
35
+ <dd>{formatValue(value)}</dd>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ function formatValue(value: unknown): string {
41
+ if (Array.isArray(value)) {
42
+ return value.join(", ");
43
+ }
44
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
45
+ return String(value);
46
+ }
47
+ return "—";
48
+ }
49
+
50
+ function stringProp(value: unknown): string | null {
51
+ return typeof value === "string" ? value : null;
52
+ }
@@ -19,6 +19,7 @@ export type FontPreference =
19
19
  | "Ubuntu Mono";
20
20
  export type FontSizePreference = "xs" | "s" | "m" | "lg" | "xl";
21
21
  export const surfaceIconNames = {
22
+ activity: true,
22
23
  apps: true,
23
24
  bookmark: true,
24
25
  brand: true,
@@ -273,8 +274,8 @@ export const text = {
273
274
  socialMedia: "Social Media",
274
275
  socialMediaIntro: "Social profiles, pages, handles, audience notes, and channel ownership are planned.",
275
276
  tasks: "Tasks",
276
- workers: "Workers",
277
- workersIntro: "Worker activity, task handoffs, event threads, and status transitions will render here as conversation events and workflow summaries.",
277
+ workers: "Pulse & Workers",
278
+ 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.",
278
279
  theme: "Theme",
279
280
  appearance: "Appearance",
280
281
  hue: "Hue",
@@ -367,7 +368,7 @@ export const navGroups: SurfaceNavGroup[] = [
367
368
  mode: "devops",
368
369
  items: [
369
370
  plannedNavItem("aiSessions", text.aiSessions, "AI chat and OpenCode sessions", "terminal"),
370
- plannedNavItem("workers", text.workers, "Worker activity and event threads", "users"),
371
+ plannedNavItem("workers", text.workers, "Pulse, worker sessions, outcomes, and resources", "activity"),
371
372
  { id: "git", label: text.localRepos, description: "~/Git explorer", icon: "folder" },
372
373
  plannedNavItem("repos", text.repos, "Unified local and remote repo context", "git"),
373
374
  { id: "projects", label: text.projects, description: "repos.json summary", icon: "git" },
@@ -11,10 +11,12 @@ export const conversationThreads: GuiConversationThread[] = [
11
11
  messages: [
12
12
  { id: "message-general-1", conversation_id: "channel-general", sender_participant_id: "participant-system", sender_kind: "system", sequence: 1, status: "sent", usage: null, created_at: "2026-06-27T12:00:00Z", edited_at: null },
13
13
  { id: "message-general-2", conversation_id: "channel-general", sender_participant_id: "participant-ai", sender_kind: "ai_assistant", sequence: 2, status: "delivered", usage: { provider_ref: "local", model_ref: "workflow-summary", input_tokens: 0, output_tokens: 0, total_tokens: 0, cost_ref: null }, created_at: "2026-06-27T12:15:00Z", edited_at: null },
14
+ { id: "message-general-3", conversation_id: "channel-general", sender_participant_id: "participant-ai", sender_kind: "ai_assistant", sequence: 3, status: "delivered", usage: { provider_ref: "tambo", model_ref: "devops-card", input_tokens: 0, output_tokens: 0, total_tokens: 0, cost_ref: null }, created_at: "2026-06-27T12:18:00Z", edited_at: null },
14
15
  ],
15
16
  parts: [
16
17
  { id: "part-general-1", message_id: "message-general-1", kind: "event_marker", ordinal: 1, text: "#general is ready for repo, deployment, review, and incident coordination.", payload_json: null, file_ref: null, source_ref: "seed" },
17
18
  { id: "part-general-2", message_id: "message-general-2", kind: "text", ordinal: 1, text: "Mention @AI DevOps or a worker to turn a thread into an audited task once write routes land.", payload_json: null, file_ref: null, source_ref: null },
19
+ { id: "part-general-3", message_id: "message-general-3", kind: "tambo_component", ordinal: 1, text: null, payload_json: { component: "TaskCard", tenant_ref: "local", session_ref: "channel-general", read_only: true, props: { title: "Integrate Tambo GenUI cards", status: "read-only preview", priority: "high", owner: "AI DevOps", repo: "aidevops", reference: "#25713" } }, file_ref: null, source_ref: "tambo:seed" },
18
20
  ],
19
21
  reactions: [{ id: "reaction-general-1", message_id: "message-general-2", participant_id: "participant-local", reaction: "ack", created_at: "2026-06-27T12:16:00Z" }],
20
22
  read_states: [{ conversation_id: "channel-general", participant_id: "participant-local", last_read_message_id: "message-general-1", last_read_sequence: 1, updated_at: "2026-06-27T12:10:00Z" }],
@@ -31,7 +33,10 @@ export const conversationThreads: GuiConversationThread[] = [
31
33
  conversation: { id: "dm-ai-devops", type: "dm", title: "AI DevOps", scope: { tenant_ref: "local", workspace_ref: "aidevops", repo_ref: null }, source_ref: "seed:dms/ai-devops", status: "read_only", created_at: "2026-06-27T00:00:00Z", updated_at: "2026-06-27T12:40:00Z" },
32
34
  participants: [{ id: "participant-dm-local", conversation_id: "dm-ai-devops", kind: "human", display_name: "Local user", identity_ref: "local:user", agent_ref: null, worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" }, { id: "participant-dm-ai", conversation_id: "dm-ai-devops", kind: "ai_assistant", display_name: "AI DevOps", identity_ref: null, agent_ref: "aidevops", worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" }],
33
35
  messages: [{ id: "message-dm-1", conversation_id: "dm-ai-devops", sender_participant_id: "participant-dm-ai", sender_kind: "ai_assistant", sequence: 1, status: "delivered", usage: null, created_at: "2026-06-27T12:40:00Z", edited_at: null }],
34
- parts: [{ id: "part-dm-1", message_id: "message-dm-1", kind: "text", ordinal: 1, text: "Direct support threads share the same message parts, participants, reactions, and read state as channels.", payload_json: null, file_ref: null, source_ref: null }],
36
+ parts: [
37
+ { id: "part-dm-1", message_id: "message-dm-1", kind: "text", ordinal: 1, text: "Direct support threads share the same message parts, participants, reactions, and read state as channels.", payload_json: null, file_ref: null, source_ref: null },
38
+ { id: "part-dm-2", message_id: "message-dm-1", kind: "approval_prompt", ordinal: 2, text: null, payload_json: { component: "ApprovalPromptCard", tenant_ref: "local", session_ref: "dm-ai-devops", read_only: true, props: { action: "Dispatch worker", reason: "Requires audited approval backend before mutation", risk: "medium", requested_by: "AI DevOps", disabled: true } }, file_ref: null, source_ref: "tambo:seed" },
39
+ ],
35
40
  reactions: [],
36
41
  read_states: [{ conversation_id: "dm-ai-devops", participant_id: "participant-dm-local", last_read_message_id: null, last_read_sequence: 0, updated_at: "2026-06-27T12:00:00Z" }],
37
42
  },
@@ -37,7 +37,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
37
37
  .map((capability) => `<li>${escapeHtml(capability.label)}: ${escapeHtml(capability.status)}</li>`)
38
38
  .join("");
39
39
  const navSections = [
40
- { heading: "Development", items: ["AI Sessions", "Workers", "Local Repos", "Repos", "Remote Repos", "Deployments", "Secrets", "AI Providers"] },
40
+ { heading: "Development", items: ["AI Sessions", "Pulse & Workers", "Local Repos", "Repos", "Remote Repos", "Deployments", "Secrets", "AI Providers"] },
41
41
  { heading: "Account", items: ["Help", "Settings", "Notifications", "Admin"] },
42
42
  { heading: "Operations", items: ["Dashboard", "Vault", "Agents file explorer", "Config", "Local Setup", "Routines"] },
43
43
  { heading: "Infrastructure", items: ["Devices", "VPNs & Proxies", "Apps", "Installation", "Registrars", "Hosts", "Servers"] },
@@ -99,6 +99,15 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
99
99
  backups: { ...statusFixture.vault.backups, ...data.vault?.backups },
100
100
  audit: { ...statusFixture.vault.audit, ...data.vault?.audit },
101
101
  },
102
+ pulse_workers: {
103
+ ...statusFixture.pulse_workers,
104
+ ...data.pulse_workers,
105
+ kpis: data.pulse_workers?.kpis ?? statusFixture.pulse_workers.kpis,
106
+ attention: data.pulse_workers?.attention ?? statusFixture.pulse_workers.attention,
107
+ filters: { ...statusFixture.pulse_workers.filters, ...data.pulse_workers?.filters },
108
+ charts: data.pulse_workers?.charts ?? statusFixture.pulse_workers.charts,
109
+ events: data.pulse_workers?.events ?? statusFixture.pulse_workers.events,
110
+ },
102
111
  capabilities: data.capabilities ?? statusFixture.capabilities,
103
112
  secrets: data.secrets ?? statusFixture.secrets,
104
113
  placeholders: data.placeholders ?? statusFixture.placeholders,
@@ -1562,6 +1562,48 @@ code {
1562
1562
  border-style: dashed;
1563
1563
  }
1564
1564
 
1565
+ .genui-card {
1566
+ background: var(--surface-elevated);
1567
+ border: 1px solid var(--border-accent);
1568
+ border-radius: 14px;
1569
+ display: grid;
1570
+ gap: 10px;
1571
+ margin-top: 4px;
1572
+ padding: 12px;
1573
+ }
1574
+
1575
+ .genui-card-invalid {
1576
+ border-color: var(--border-warning);
1577
+ }
1578
+
1579
+ .genui-card h3 {
1580
+ margin: 0;
1581
+ }
1582
+
1583
+ .genui-card dl {
1584
+ display: grid;
1585
+ gap: 8px;
1586
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
1587
+ margin: 0;
1588
+ }
1589
+
1590
+ .genui-card dt {
1591
+ color: var(--text-muted);
1592
+ font-size: 0.76rem;
1593
+ text-transform: uppercase;
1594
+ }
1595
+
1596
+ .genui-card dd {
1597
+ color: var(--text-secondary);
1598
+ margin: 0;
1599
+ }
1600
+
1601
+ .genui-deferred {
1602
+ border-top: 1px dashed var(--border);
1603
+ color: var(--text-muted);
1604
+ padding-top: 8px;
1605
+ }
1606
+
1565
1607
  .conversation-list-panel {
1566
1608
  grid-template-rows: auto auto auto minmax(0, 1fr);
1567
1609
  }
@@ -2179,6 +2221,157 @@ code {
2179
2221
  margin-bottom: 16px;
2180
2222
  }
2181
2223
 
2224
+ .pulse-workers-surface {
2225
+ display: grid;
2226
+ gap: 16px;
2227
+ }
2228
+
2229
+ .pulse-hero {
2230
+ background:
2231
+ linear-gradient(135deg, color-mix(in srgb, var(--accent) 18%, transparent), transparent 64%),
2232
+ var(--surface-elevated);
2233
+ }
2234
+
2235
+ .pulse-scope-strip,
2236
+ .pulse-filter-row {
2237
+ display: flex;
2238
+ flex-wrap: wrap;
2239
+ gap: 8px;
2240
+ }
2241
+
2242
+ .pulse-scope-strip span,
2243
+ .pulse-filter-row button {
2244
+ background: var(--surface-muted);
2245
+ border: 1px solid var(--border);
2246
+ border-radius: 999px;
2247
+ color: var(--text-secondary);
2248
+ padding: 7px 10px;
2249
+ }
2250
+
2251
+ .pulse-scope-strip strong {
2252
+ color: var(--accent);
2253
+ margin-right: 6px;
2254
+ text-transform: uppercase;
2255
+ }
2256
+
2257
+ .pulse-kpi-grid,
2258
+ .pulse-layout {
2259
+ display: grid;
2260
+ gap: 12px;
2261
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
2262
+ }
2263
+
2264
+ .pulse-kpi-card {
2265
+ display: grid;
2266
+ gap: 8px;
2267
+ }
2268
+
2269
+ .pulse-kpi-card span,
2270
+ .pulse-chart-panel p,
2271
+ .pulse-drilldown-panel p,
2272
+ .pulse-actions-panel p {
2273
+ color: var(--text-secondary);
2274
+ }
2275
+
2276
+ .pulse-kpi-card strong {
2277
+ color: var(--accent);
2278
+ font-size: clamp(2rem, 5vw, 3rem);
2279
+ letter-spacing: -0.08em;
2280
+ line-height: 1;
2281
+ }
2282
+
2283
+ .pulse-attention-panel,
2284
+ .pulse-chart-panel,
2285
+ .pulse-activity-panel,
2286
+ .pulse-drilldown-panel,
2287
+ .pulse-actions-panel {
2288
+ display: grid;
2289
+ gap: 12px;
2290
+ }
2291
+
2292
+ .pulse-attention-panel ul {
2293
+ display: grid;
2294
+ gap: 8px;
2295
+ margin: 0;
2296
+ padding-left: 18px;
2297
+ }
2298
+
2299
+ .pulse-chart-placeholder {
2300
+ align-items: end;
2301
+ background: var(--surface-muted);
2302
+ border: 1px solid var(--border);
2303
+ border-radius: 16px;
2304
+ display: grid;
2305
+ gap: 8px;
2306
+ grid-template-columns: repeat(6, minmax(0, 1fr));
2307
+ min-height: 180px;
2308
+ padding: 14px;
2309
+ }
2310
+
2311
+ .pulse-chart-placeholder span {
2312
+ align-items: end;
2313
+ background: color-mix(in srgb, var(--accent) 26%, transparent);
2314
+ border: 1px solid var(--border-accent);
2315
+ border-radius: 12px 12px 4px 4px;
2316
+ color: var(--accent-strong);
2317
+ display: flex;
2318
+ font-size: 0.75rem;
2319
+ font-weight: 900;
2320
+ justify-content: center;
2321
+ min-height: calc(40px + var(--bar-index, 1) * 18px);
2322
+ padding: 8px 4px;
2323
+ text-transform: uppercase;
2324
+ }
2325
+
2326
+ .pulse-chart-placeholder span:nth-child(2) { --bar-index: 3; }
2327
+ .pulse-chart-placeholder span:nth-child(3) { --bar-index: 5; }
2328
+ .pulse-chart-placeholder span:nth-child(4) { --bar-index: 2; }
2329
+ .pulse-chart-placeholder span:nth-child(5) { --bar-index: 4; }
2330
+ .pulse-chart-placeholder span:nth-child(6) { --bar-index: 3; }
2331
+
2332
+ .pulse-activity-table {
2333
+ background: var(--surface-muted);
2334
+ border: 1px solid var(--border);
2335
+ border-radius: 16px;
2336
+ display: grid;
2337
+ overflow: hidden;
2338
+ }
2339
+
2340
+ .pulse-activity-table thead,
2341
+ .pulse-activity-table tbody {
2342
+ display: grid;
2343
+ }
2344
+
2345
+ .pulse-activity-row {
2346
+ display: grid;
2347
+ gap: 10px;
2348
+ grid-template-columns: 0.7fr 1fr 1.4fr 1fr 1.2fr 1.2fr;
2349
+ padding: 12px 14px;
2350
+ }
2351
+
2352
+ .pulse-activity-row + .pulse-activity-row {
2353
+ border-top: 1px solid var(--border);
2354
+ }
2355
+
2356
+ .pulse-activity-row th,
2357
+ .pulse-activity-row td {
2358
+ font-weight: inherit;
2359
+ text-align: left;
2360
+ }
2361
+
2362
+ .pulse-activity-header {
2363
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
2364
+ color: var(--text-secondary);
2365
+ font-size: 0.78rem;
2366
+ font-weight: 900;
2367
+ text-transform: uppercase;
2368
+ }
2369
+
2370
+ .pulse-actions-panel button,
2371
+ .pulse-attention-panel button {
2372
+ justify-self: start;
2373
+ }
2374
+
2182
2375
  .apps-surface .compact-notice {
2183
2376
  margin-bottom: 0;
2184
2377
  }
@@ -3624,10 +3817,25 @@ button.managed-toggle:focus-visible {
3624
3817
  .data-row,
3625
3818
  .editable-row,
3626
3819
  .managed-app-details,
3820
+ .pulse-activity-row,
3627
3821
  .provider-account-list li {
3628
3822
  grid-template-columns: 1fr;
3629
3823
  }
3630
3824
 
3825
+ .pulse-activity-header {
3826
+ display: none;
3827
+ }
3828
+
3829
+ .pulse-activity-row {
3830
+ background: var(--surface-elevated);
3831
+ }
3832
+
3833
+ .pulse-activity-row td::before {
3834
+ color: var(--text-muted);
3835
+ content: attr(data-label) ": ";
3836
+ font-weight: 900;
3837
+ }
3838
+
3631
3839
  .pill-tabs {
3632
3840
  width: 100%;
3633
3841
  }
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.37
15
+ # Version: 3.29.39
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)