aidevops 3.39.55 → 3.39.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/status-pulse-workers-insights.ts +107 -0
- package/packages/gui-api/src/status-pulse-workers.ts +1 -64
- package/packages/gui-web/src/AiSessionsSurface.tsx +110 -0
- package/packages/gui-web/src/AppWorkspace.tsx +2 -107
- package/packages/gui-web/src/app-model.ts +17 -67
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ token efficiency, and quality control built in.**
|
|
|
25
25
|
[](https://qlty.sh/gh/marcusquinn/projects/aidevops)
|
|
26
26
|
[](https://app.codacy.com/gh/marcusquinn/aidevops/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
27
27
|
[](https://opensource.org/licenses/MIT)
|
|
28
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
29
29
|
[](https://www.npmjs.com/package/aidevops)
|
|
30
30
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
31
31
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.39.
|
|
1
|
+
3.39.57
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { GuiPulseResourceSnapshot, GuiPulseSystemicFinding, GuiPulseWorkerActivityEvent } from "../../gui-shared/src";
|
|
2
|
+
import { type MetricRecord, numberValue } from "./status-pulse-workers-values";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SCOPE = "local aidevops telemetry";
|
|
5
|
+
const SLOW_EVENT_MS = 1_800_000;
|
|
6
|
+
|
|
7
|
+
type Finding = GuiPulseSystemicFinding;
|
|
8
|
+
type PulseEvent = GuiPulseWorkerActivityEvent;
|
|
9
|
+
|
|
10
|
+
function finding(...args: [string, Finding["kind"], Finding["severity"], string, string, string, PulseEvent[], string, string, number, Finding["confidence"], string?]): Finding {
|
|
11
|
+
const [id, kind, severity, title, detail, likelyCause, events, recommendation, metricLabel, sampleSize, confidence, comparisonLabel = "selected period scoped to active filters"] = args;
|
|
12
|
+
return { id: `insight-${id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}`, kind, severity, title, detail, likely_cause: likelyCause, evidence_refs: evidenceRefs(events), event_refs: events.map((event) => event.id).slice(0, 6), confidence, recommendation, metric_label: metricLabel, period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: comparisonLabel, sample_size: sampleSize, primary_action: "create_systemic_fix" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function groupEvents(events: PulseEvent[], keyFor: (event: PulseEvent) => string): Map<string, PulseEvent[]> {
|
|
16
|
+
const groups = new Map<string, PulseEvent[]>();
|
|
17
|
+
for (const event of events) {
|
|
18
|
+
const key = keyFor(event);
|
|
19
|
+
groups.set(key, [...(groups.get(key) ?? []), event]);
|
|
20
|
+
}
|
|
21
|
+
return groups;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function evidenceRefs(events: PulseEvent[]): string[] {
|
|
25
|
+
return events.flatMap((event) => [event.repo_ref, event.issue_ref, event.pull_request_ref, event.worker_session_ref, event.command_job_ref].filter(Boolean) as string[]).slice(0, 8);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sumCost(records: MetricRecord[]): number {
|
|
29
|
+
return records.reduce((sum, record) => sum + (numberValue(record.estimated_cost_usd ?? record.cost_usd) ?? 0), 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isElevated(resource: GuiPulseResourceSnapshot): boolean {
|
|
33
|
+
return resource.pressure === "medium" || resource.pressure === "high";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isFailedOrBlocked(event: PulseEvent): boolean {
|
|
37
|
+
return event.outcome === "failed" || event.outcome === "blocked";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function thirdPartyWaitingFinding(events: PulseEvent[]): Finding[] {
|
|
41
|
+
const waiting = events.filter((event) => event.issue_origin === "third_party" && (event.outcome === "needs_maintainer_review" || event.outcome === "blocked" || event.outcome === "in_progress"));
|
|
42
|
+
if (waiting.length === 0) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return [finding("third-party-waiting", "third_party_waiting", "warning", "Third-party issues waiting", `${waiting.length} community-origin issue events need triage or resolution in the selected period.`, "Community reports are separated from aidevops-created task handling and may need a maintainer decision before dispatch.", waiting, "Create a systemic triage task that preserves non-collaborator trust boundaries and adds first-response thresholds.", "community waiting count", waiting.length, "high")];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function repeatedFailureFindings(events: PulseEvent[]): Finding[] {
|
|
49
|
+
const failedOrBlocked = events.filter(isFailedOrBlocked);
|
|
50
|
+
const groups = groupEvents(failedOrBlocked, (event) => [event.repo_ref ?? "repo:unknown", event.usage?.provider ?? "provider:unknown", event.resources[0]?.kind ?? "resource:unknown"].join("|"));
|
|
51
|
+
const findings: Finding[] = [];
|
|
52
|
+
for (const [key, group] of groups) {
|
|
53
|
+
if (group.length > 1) {
|
|
54
|
+
findings.push(finding(`repeated-${key}`, "repeated_failure", "critical", "Repeated failure pattern", `${group.length} failed or blocked runs share repo/provider/resource context.`, "A repeated worker blindspot is likely being retried as individual failures instead of being converted into a shared guardrail or helper fix.", group, "Create a systemic fix task with grouped evidence, affected repo/provider/resource, and a focused verification command.", key.replaceAll("|", " · "), group.length, "medium"));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function weakVerificationFinding(events: PulseEvent[]): Finding[] {
|
|
61
|
+
const weak = events.filter((event) => event.drilldown_sections.some((section) => section.label === "Verification" && /missing|weak|not recorded/i.test(section.body)));
|
|
62
|
+
if (weak.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
return [finding("weak-verification", "weak_verification", "warning", "No-verification outcomes", `${weak.length} events lack strong verification metadata.`, "Workers may be completing or blocking without durable command evidence in the telemetry stream.", weak, "Create a systemic fix task to require verification command capture in worker outcome records.", "verification coverage", weak.length, "medium")];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resourcePressureFinding(events: PulseEvent[], resources: GuiPulseResourceSnapshot[]): Finding[] {
|
|
69
|
+
const pressureEvents = events.filter((event) => event.resources.some(isElevated));
|
|
70
|
+
if (pressureEvents.length === 0 && !resources.some(isElevated)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const severity = resources.some((resource) => resource.pressure === "high") ? "critical" : "warning";
|
|
74
|
+
return [finding("resource-pressure", "resource_pressure", severity, "Resource allowance pressure", `${pressureEvents.length} events overlapped medium/high resource pressure; ${resources.length} resource samples are in scope.`, "Provider quota, GitHub API, queue, CI, or local resource pressure can inflate retry cost and slow dispatch-to-merge cycles.", pressureEvents, "Create a systemic capacity task that records allowance snapshots before redispatching failed workers.", "resource pressure", Math.max(pressureEvents.length, resources.length), "medium")];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function costFindings(events: PulseEvent[], dayMetrics: MetricRecord[], previousDayMetrics: MetricRecord[]): Finding[] {
|
|
78
|
+
const currentCost = sumCost(dayMetrics);
|
|
79
|
+
const previousCost = sumCost(previousDayMetrics);
|
|
80
|
+
const failedCostEvents = events.filter((event) => isFailedOrBlocked(event) && event.usage?.estimated_cost_ref != null);
|
|
81
|
+
if (currentCost > 0 && previousCost > 0 && currentCost >= previousCost * 1.5) {
|
|
82
|
+
return [finding("cost-spike", "cost_spike", "warning", "Cost spike versus previous period", `Estimated selected-period cost is ${currentCost.toFixed(2)} versus ${previousCost.toFixed(2)} in the previous equivalent window.`, "Retry loops or expensive models may be consuming allowance faster than successful outcomes justify.", events, "Create a systemic cost-control task that ties model/provider selection to outcome quality and retry count.", "estimated USD", dayMetrics.length, "medium", `previous equivalent period: ${previousCost.toFixed(2)}`)];
|
|
83
|
+
}
|
|
84
|
+
if (failedCostEvents.length > 0) {
|
|
85
|
+
return [finding("failed-run-cost", "high_retry_cost", "warning", "Failed-run cost attached to outcomes", `${failedCostEvents.length} failed or blocked events include token/cost metadata.`, "Cost is being spent on outcomes that did not merge, close, defer, or create follow-up work.", failedCostEvents, "Create a systemic retry-budget task that escalates or changes strategy before another costly rerun.", "failed-run cost samples", failedCostEvents.length, "medium")];
|
|
86
|
+
}
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function slowBottleneckFinding(events: PulseEvent[]): Finding[] {
|
|
91
|
+
const slow = events.filter((event) => (event.duration_ms ?? 0) >= SLOW_EVENT_MS);
|
|
92
|
+
if (slow.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return [finding("slow-bottlenecks", "slow_bottleneck", "warning", "Slow dispatch-to-outcome bottlenecks", `${slow.length} events exceeded 30 minutes.`, "Long-running dispatch, review, or merge gates can hide queue/concurrency or CI runner bottlenecks.", slow, "Create a systemic bottleneck task that separates dispatch-to-PR and PR-to-merge timing evidence.", "duration >=30m", slow.length, "medium")];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildInsights(events: PulseEvent[], dayMetrics: MetricRecord[], previousDayMetrics: MetricRecord[], resources: GuiPulseResourceSnapshot[]): Finding[] {
|
|
99
|
+
return [
|
|
100
|
+
...thirdPartyWaitingFinding(events),
|
|
101
|
+
...repeatedFailureFindings(events),
|
|
102
|
+
...weakVerificationFinding(events),
|
|
103
|
+
...resourcePressureFinding(events, resources),
|
|
104
|
+
...costFindings(events, dayMetrics, previousDayMetrics),
|
|
105
|
+
...slowBottleneckFinding(events),
|
|
106
|
+
].slice(0, 8);
|
|
107
|
+
}
|
|
@@ -3,7 +3,6 @@ import { join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type GuiPulseResourceKind,
|
|
5
5
|
type GuiPulseResourceSnapshot,
|
|
6
|
-
type GuiPulseSystemicFinding,
|
|
7
6
|
type GuiPulseWorkerActionSummary,
|
|
8
7
|
type GuiPulseWorkerActivityEvent,
|
|
9
8
|
type GuiPulseWorkerChartSeries,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
} from "../../gui-shared/src";
|
|
16
15
|
import { collapseHome, expandHome, formatEpochField, isRecord, readJsonObject, stringField } from "./status-adapter-utils";
|
|
17
16
|
import { buildAttention } from "./status-pulse-workers-attention";
|
|
17
|
+
import { buildInsights } from "./status-pulse-workers-insights";
|
|
18
18
|
import { authorAssociationFromRecord, isHealthyOutcome, issueOriginFromRecord, outcomeFromRecord, providerFromString, severityFromStatus, statusFromOutcome } from "./status-pulse-workers-classifiers";
|
|
19
19
|
import { costRef, countOptions, durationMsFromRecord, type MetricRecord, numberValue, recordTimeMs, stringOrNumber, summaryFromRecord } from "./status-pulse-workers-values";
|
|
20
20
|
|
|
@@ -218,65 +218,6 @@ function buildEvents(records: MetricRecord[], resources: GuiPulseResourceSnapsho
|
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function buildInsights(events: GuiPulseWorkerActivityEvent[], dayMetrics: MetricRecord[], previousDayMetrics: MetricRecord[], resources: GuiPulseResourceSnapshot[]): GuiPulseSystemicFinding[] {
|
|
222
|
-
const findings: GuiPulseSystemicFinding[] = [];
|
|
223
|
-
const thirdPartyWaiting = events.filter((event) => event.issue_origin === "third_party" && (event.outcome === "needs_maintainer_review" || event.outcome === "blocked" || event.outcome === "in_progress"));
|
|
224
|
-
if (thirdPartyWaiting.length > 0) {
|
|
225
|
-
findings.push(finding("third-party-waiting", "third_party_waiting", "warning", "Third-party issues waiting", `${thirdPartyWaiting.length} community-origin issue events need triage or resolution in the selected period.`, "Community reports are separated from aidevops-created task handling and may need a maintainer decision before dispatch.", thirdPartyWaiting, "Create a systemic triage task that preserves non-collaborator trust boundaries and adds first-response thresholds.", "community waiting count", thirdPartyWaiting.length, "high"));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const failedOrBlocked = events.filter((event) => event.outcome === "failed" || event.outcome === "blocked");
|
|
229
|
-
for (const [key, group] of groupEvents(failedOrBlocked, (event) => [event.repo_ref ?? "repo:unknown", event.usage?.provider ?? "provider:unknown", event.resources[0]?.kind ?? "resource:unknown"].join("|"))) {
|
|
230
|
-
if (group.length > 1) {
|
|
231
|
-
findings.push(finding(`repeated-${key}`, "repeated_failure", "critical", "Repeated failure pattern", `${group.length} failed or blocked runs share repo/provider/resource context.`, "A repeated worker blindspot is likely being retried as individual failures instead of being converted into a shared guardrail or helper fix.", group, "Create a systemic fix task with grouped evidence, affected repo/provider/resource, and a focused verification command.", key.replaceAll("|", " · "), group.length, "medium"));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const weakVerification = events.filter((event) => event.drilldown_sections.some((section) => section.label === "Verification" && /missing|weak|not recorded/i.test(section.body)));
|
|
236
|
-
if (weakVerification.length > 0) {
|
|
237
|
-
findings.push(finding("weak-verification", "weak_verification", "warning", "No-verification outcomes", `${weakVerification.length} events lack strong verification metadata.`, "Workers may be completing or blocking without durable command evidence in the telemetry stream.", weakVerification, "Create a systemic fix task to require verification command capture in worker outcome records.", "verification coverage", weakVerification.length, "medium"));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const pressureEvents = events.filter((event) => event.resources.some((resource) => resource.pressure === "medium" || resource.pressure === "high"));
|
|
241
|
-
if (pressureEvents.length > 0 || resources.some((resource) => resource.pressure === "medium" || resource.pressure === "high")) {
|
|
242
|
-
findings.push(finding("resource-pressure", "resource_pressure", resources.some((resource) => resource.pressure === "high") ? "critical" : "warning", "Resource allowance pressure", `${pressureEvents.length} events overlapped medium/high resource pressure; ${resources.length} resource samples are in scope.`, "Provider quota, GitHub API, queue, CI, or local resource pressure can inflate retry cost and slow dispatch-to-merge cycles.", pressureEvents, "Create a systemic capacity task that records allowance snapshots before redispatching failed workers.", "resource pressure", Math.max(pressureEvents.length, resources.length), "medium"));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const currentCost = sumCost(dayMetrics);
|
|
246
|
-
const previousCost = sumCost(previousDayMetrics);
|
|
247
|
-
const failedCostEvents = events.filter((event) => (event.outcome === "failed" || event.outcome === "blocked") && event.usage?.estimated_cost_ref != null);
|
|
248
|
-
if (currentCost > 0 && previousCost > 0 && currentCost >= previousCost * 1.5) {
|
|
249
|
-
findings.push(finding("cost-spike", "cost_spike", "warning", "Cost spike versus previous period", `Estimated selected-period cost is ${currentCost.toFixed(2)} versus ${previousCost.toFixed(2)} in the previous equivalent window.`, "Retry loops or expensive models may be consuming allowance faster than successful outcomes justify.", events, "Create a systemic cost-control task that ties model/provider selection to outcome quality and retry count.", "estimated USD", dayMetrics.length, "medium", `previous equivalent period: ${previousCost.toFixed(2)}`));
|
|
250
|
-
} else if (failedCostEvents.length > 0) {
|
|
251
|
-
findings.push(finding("failed-run-cost", "high_retry_cost", "warning", "Failed-run cost attached to outcomes", `${failedCostEvents.length} failed or blocked events include token/cost metadata.`, "Cost is being spent on outcomes that did not merge, close, defer, or create follow-up work.", failedCostEvents, "Create a systemic retry-budget task that escalates or changes strategy before another costly rerun.", "failed-run cost samples", failedCostEvents.length, "medium"));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const slowEvents = events.filter((event) => (event.duration_ms ?? 0) >= 1_800_000);
|
|
255
|
-
if (slowEvents.length > 0) {
|
|
256
|
-
findings.push(finding("slow-bottlenecks", "slow_bottleneck", "warning", "Slow dispatch-to-outcome bottlenecks", `${slowEvents.length} events exceeded 30 minutes.`, "Long-running dispatch, review, or merge gates can hide queue/concurrency or CI runner bottlenecks.", slowEvents, "Create a systemic bottleneck task that separates dispatch-to-PR and PR-to-merge timing evidence.", "duration >=30m", slowEvents.length, "medium"));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return findings.slice(0, 8);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function finding(...args: [string, GuiPulseSystemicFinding["kind"], GuiPulseSystemicFinding["severity"], string, string, string, GuiPulseWorkerActivityEvent[], string, string, number, GuiPulseSystemicFinding["confidence"], string?]): GuiPulseSystemicFinding {
|
|
263
|
-
const [id, kind, severity, title, detail, likelyCause, events, recommendation, metricLabel, sampleSize, confidence, comparisonLabel = "selected period scoped to active filters"] = args;
|
|
264
|
-
return { id: `insight-${id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()}`, kind, severity, title, detail, likely_cause: likelyCause, evidence_refs: evidenceRefs(events), event_refs: events.map((event) => event.id).slice(0, 6), confidence, recommendation, metric_label: metricLabel, period_label: "Last 24h", scope_label: DEFAULT_SCOPE, comparison_label: comparisonLabel, sample_size: sampleSize, primary_action: "create_systemic_fix" };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function groupEvents(events: GuiPulseWorkerActivityEvent[], keyFor: (event: GuiPulseWorkerActivityEvent) => string): Map<string, GuiPulseWorkerActivityEvent[]> {
|
|
268
|
-
const groups = new Map<string, GuiPulseWorkerActivityEvent[]>();
|
|
269
|
-
for (const event of events) {
|
|
270
|
-
const key = keyFor(event);
|
|
271
|
-
groups.set(key, [...(groups.get(key) ?? []), event]);
|
|
272
|
-
}
|
|
273
|
-
return groups;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function evidenceRefs(events: GuiPulseWorkerActivityEvent[]): string[] {
|
|
277
|
-
return events.flatMap((event) => [event.repo_ref, event.issue_ref, event.pull_request_ref, event.worker_session_ref, event.command_job_ref].filter(Boolean) as string[]).slice(0, 8);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
221
|
function verificationSummary(record: MetricRecord): string {
|
|
281
222
|
const value = stringField(record, "verification") ?? stringField(record, "testing") ?? stringField(record, "verification_status");
|
|
282
223
|
if (typeof value === "string" && value.length > 0) return value;
|
|
@@ -289,10 +230,6 @@ function systemicFixRecommendation(record: MetricRecord, outcome: string): strin
|
|
|
289
230
|
return "Create follow-up only when the same blindspot repeats across events in the selected period.";
|
|
290
231
|
}
|
|
291
232
|
|
|
292
|
-
function sumCost(records: MetricRecord[]): number {
|
|
293
|
-
return records.reduce((sum, record) => sum + (numberValue(record.estimated_cost_usd ?? record.cost_usd) ?? 0), 0);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
233
|
function buildFilters(events: GuiPulseWorkerActivityEvent[]): GuiPulseWorkerSummary["filters"] {
|
|
297
234
|
return {
|
|
298
235
|
repos: countOptions(events.map((event) => event.repo_ref).filter(Boolean) as string[], "repo:"),
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { FiFileText, FiHash, FiPaperclip, FiTerminal } from "react-icons/fi";
|
|
3
|
+
import type { GuiStatusData } from "../../gui-shared/src";
|
|
4
|
+
import { text } from "./app-model";
|
|
5
|
+
import { TamboConversationPart } from "./GenUiCards";
|
|
6
|
+
|
|
7
|
+
const sessionActions = ["New", "Rename", "Pin", "Archive", "Delete", "Share", "Export"] as const;
|
|
8
|
+
|
|
9
|
+
export function sessionForRepo(status: GuiStatusData, repoPathRef: string, selectedSessionId: string | undefined) {
|
|
10
|
+
const sessions = status.opencode_sessions?.sessions ?? [];
|
|
11
|
+
return sessions.find((session) => session.id_ref === selectedSessionId && session.repo_path_ref === repoPathRef) ?? sessions.find((session) => session.repo_path_ref === repoPathRef);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function AiSessionsSurface({ selectedRepoIndex, selectedSessionId, status }: { selectedRepoIndex: number; selectedSessionId?: string; status: GuiStatusData }): ReactElement {
|
|
15
|
+
const selectedRepo = status.local_repos?.repos?.[selectedRepoIndex] ?? status.local_repos?.repos?.[0];
|
|
16
|
+
const selectedSession = selectedRepo === undefined ? undefined : sessionForRepo(status, selectedRepo.path_ref, selectedSessionId);
|
|
17
|
+
const providerOptions = status.oauth_pool?.providers?.filter((provider) => provider.configured || provider.total > 0) ?? [];
|
|
18
|
+
const sessionTitle = selectedSession?.title ?? "AI session bridge";
|
|
19
|
+
const sessionMeta = selectedSession ? `${selectedSession.model} via ${selectedSession.agent}` : "No OpenCode session metadata selected";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className="chat-surface ai-sessions-surface" aria-label={text.aiSessions} data-tour="ai-sessions-surface">
|
|
23
|
+
<aside className="chat-thread-panel" aria-label="AI session controls" data-tour="ai-session-list">
|
|
24
|
+
<header className="chat-thread-header">
|
|
25
|
+
<div>
|
|
26
|
+
<p className="eyebrow">{text.aiSessions}</p>
|
|
27
|
+
<h2><FiTerminal aria-hidden="true" /> {selectedRepo?.name ?? "Local workspace"}</h2>
|
|
28
|
+
</div>
|
|
29
|
+
<span className="count-pill">{text.readOnly}</span>
|
|
30
|
+
</header>
|
|
31
|
+
<div className="notice compact-notice">New, rename, pin, archive, delete, share, and export are visible but disabled until audited AI session write routes land.</div>
|
|
32
|
+
<div className="sidebar-history-controls">
|
|
33
|
+
{sessionActions.map((action) => <button disabled key={action} title={`${action} needs the AI session write adapter`} type="button">{action}</button>)}
|
|
34
|
+
</div>
|
|
35
|
+
<label className="settings-form">
|
|
36
|
+
<span>Model/provider</span>
|
|
37
|
+
<select
|
|
38
|
+
disabled
|
|
39
|
+
defaultValue={providerOptions[0]?.provider ?? "local"}
|
|
40
|
+
title="Model switching needs the Turbostarter AI transport adapter."
|
|
41
|
+
>
|
|
42
|
+
{providerOptions.length === 0 ? (
|
|
43
|
+
<option value="local">Provider setup required</option>
|
|
44
|
+
) : (
|
|
45
|
+
providerOptions.map((provider) => (
|
|
46
|
+
<option key={provider.provider} value={provider.provider}>
|
|
47
|
+
{provider.provider} ({provider.available}/{provider.total})
|
|
48
|
+
</option>
|
|
49
|
+
))
|
|
50
|
+
)}
|
|
51
|
+
</select>
|
|
52
|
+
</label>
|
|
53
|
+
</aside>
|
|
54
|
+
<div className="chat-thread-panel" data-tour="ai-session-transcript">
|
|
55
|
+
<header className="chat-thread-header">
|
|
56
|
+
<div>
|
|
57
|
+
<p className="eyebrow">{sessionMeta}</p>
|
|
58
|
+
<h2><FiHash aria-hidden="true" /> {sessionTitle}</h2>
|
|
59
|
+
</div>
|
|
60
|
+
<button disabled title="Convert to worker task needs dispatch backend support" type="button">Create worker task</button>
|
|
61
|
+
</header>
|
|
62
|
+
<div className="chat-message-list" data-tour="message-scroller">
|
|
63
|
+
<MessageMarker label="Session status" detail="MessageScroller-compatible transcript; auto-scroll must pause when the reader scrolls away from the latest message." />
|
|
64
|
+
<ChatBubble
|
|
65
|
+
speaker="assistant"
|
|
66
|
+
title={sessionTitle}
|
|
67
|
+
body={
|
|
68
|
+
selectedSession
|
|
69
|
+
? `Most recent OpenCode session metadata: ${sessionMeta}, updated ${selectedSession.updated_at}. Message payloads stay out of the status API until the Turbostarter AI chat bridge lands.`
|
|
70
|
+
: "OpenCode session creation and continuation need an audited write route. This surface is ready for the Turbostarter AI adapter once connected."
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
<AttachmentCard label="Context attachment" detail={selectedRepo ? `Selected repo context: ${selectedRepo.path_ref}` : "No local repos were discovered yet."} />
|
|
74
|
+
<MessageMarker label="Reasoning" detail="Reasoning disclosure, tool status, sources, retries, errors, and token usage render here when the AI transport supplies those parts." />
|
|
75
|
+
<ToolStatusCard />
|
|
76
|
+
<TamboConversationPart
|
|
77
|
+
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" }}
|
|
78
|
+
scope={{ tenant_ref: "local", workspace_ref: "aidevops", repo_ref: selectedRepo?.path_ref ?? null }}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<form className="chat-composer ai-composer" aria-label="AI prompt composer" data-tour="ai-composer">
|
|
82
|
+
<button disabled title="Attachments need the audited upload/context adapter" type="button"><FiPaperclip aria-hidden="true" /> Attach</button>
|
|
83
|
+
<textarea disabled placeholder={text.chatInputPlaceholder} />
|
|
84
|
+
<button disabled title="Sending needs the Turbostarter AI transport adapter" type="button">Send</button>
|
|
85
|
+
</form>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function MessageMarker({ detail, label }: { detail: string; label: string }) {
|
|
92
|
+
return <article className="chat-bubble assistant"><strong>{label}</strong><p>{detail}</p></article>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function AttachmentCard({ detail, label }: { detail: string; label: string }) {
|
|
96
|
+
return <article className="chat-bubble user"><strong><FiFileText aria-hidden="true" /> {label}</strong><p>{detail}</p></article>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ToolStatusCard() {
|
|
100
|
+
return <article className="chat-bubble assistant"><strong>Tool status</strong><p>Tool calls, source citations, retries, errors, and usage metadata are reserved for the shared conversation part model.</p></article>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistant" | "user"; title: string }) {
|
|
104
|
+
return (
|
|
105
|
+
<article className={`chat-bubble ${speaker}`}>
|
|
106
|
+
<strong>{title}</strong>
|
|
107
|
+
<p>{body}</p>
|
|
108
|
+
</article>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { type ReactElement, type ReactNode, useEffect, useState } from "react";
|
|
3
|
-
import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand,
|
|
3
|
+
import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiInfo, FiLock, FiLogOut, FiMap, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUnlock, FiUser } from "react-icons/fi";
|
|
4
4
|
import type { GuiFileRootId, GuiNotificationSummary, GuiStatusData } from "../../gui-shared/src";
|
|
5
5
|
import { SurfaceGlyph } from "./AppNavigation";
|
|
6
6
|
import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
|
|
@@ -8,7 +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 {
|
|
11
|
+
import { AiSessionsSurface, sessionForRepo } from "./AiSessionsSurface";
|
|
12
12
|
import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
|
|
13
13
|
import { PulseWorkersSurface } from "./PulseWorkersSurface";
|
|
14
14
|
import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
|
|
@@ -263,111 +263,6 @@ function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selec
|
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
function AiSessionsSurface({ selectedRepoIndex, selectedSessionId, status }: { selectedRepoIndex: number; selectedSessionId?: string; status: GuiStatusData }): ReactElement {
|
|
267
|
-
const selectedRepo = status.local_repos?.repos?.[selectedRepoIndex] ?? status.local_repos?.repos?.[0];
|
|
268
|
-
const selectedSession = selectedRepo === undefined ? undefined : sessionForRepo(status, selectedRepo.path_ref, selectedSessionId);
|
|
269
|
-
const providerOptions = status.oauth_pool?.providers?.filter((provider) => provider.configured || provider.total > 0) ?? [];
|
|
270
|
-
const sessionTitle = selectedSession?.title ?? "AI session bridge";
|
|
271
|
-
const sessionMeta = selectedSession ? `${selectedSession.model} via ${selectedSession.agent}` : "No OpenCode session metadata selected";
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<section className="chat-surface ai-sessions-surface" aria-label={text.aiSessions} data-tour="ai-sessions-surface">
|
|
275
|
-
<aside className="chat-thread-panel" aria-label="AI session controls" data-tour="ai-session-list">
|
|
276
|
-
<header className="chat-thread-header">
|
|
277
|
-
<div>
|
|
278
|
-
<p className="eyebrow">{text.aiSessions}</p>
|
|
279
|
-
<h2><FiTerminal aria-hidden="true" /> {selectedRepo?.name ?? "Local workspace"}</h2>
|
|
280
|
-
</div>
|
|
281
|
-
<span className="count-pill">{text.readOnly}</span>
|
|
282
|
-
</header>
|
|
283
|
-
<div className="notice compact-notice">New, rename, pin, archive, delete, share, and export are visible but disabled until audited AI session write routes land.</div>
|
|
284
|
-
<div className="sidebar-history-controls">
|
|
285
|
-
{sessionActions.map((action) => <button disabled key={action} title={`${action} needs the AI session write adapter`} type="button">{action}</button>)}
|
|
286
|
-
</div>
|
|
287
|
-
<label className="settings-form">
|
|
288
|
-
<span>Model/provider</span>
|
|
289
|
-
<select
|
|
290
|
-
disabled
|
|
291
|
-
defaultValue={providerOptions[0]?.provider ?? "local"}
|
|
292
|
-
title="Model switching needs the Turbostarter AI transport adapter."
|
|
293
|
-
>
|
|
294
|
-
{providerOptions.length === 0 ? (
|
|
295
|
-
<option value="local">Provider setup required</option>
|
|
296
|
-
) : (
|
|
297
|
-
providerOptions.map((provider) => (
|
|
298
|
-
<option key={provider.provider} value={provider.provider}>
|
|
299
|
-
{provider.provider} ({provider.available}/{provider.total})
|
|
300
|
-
</option>
|
|
301
|
-
))
|
|
302
|
-
)}
|
|
303
|
-
</select>
|
|
304
|
-
</label>
|
|
305
|
-
</aside>
|
|
306
|
-
<div className="chat-thread-panel" data-tour="ai-session-transcript">
|
|
307
|
-
<header className="chat-thread-header">
|
|
308
|
-
<div>
|
|
309
|
-
<p className="eyebrow">{sessionMeta}</p>
|
|
310
|
-
<h2><FiHash aria-hidden="true" /> {sessionTitle}</h2>
|
|
311
|
-
</div>
|
|
312
|
-
<button disabled title="Convert to worker task needs dispatch backend support" type="button">Create worker task</button>
|
|
313
|
-
</header>
|
|
314
|
-
<div className="chat-message-list" data-tour="message-scroller">
|
|
315
|
-
<MessageMarker label="Session status" detail="MessageScroller-compatible transcript; auto-scroll must pause when the reader scrolls away from the latest message." />
|
|
316
|
-
<ChatBubble
|
|
317
|
-
speaker="assistant"
|
|
318
|
-
title={sessionTitle}
|
|
319
|
-
body={
|
|
320
|
-
selectedSession
|
|
321
|
-
? `Most recent OpenCode session metadata: ${sessionMeta}, updated ${selectedSession.updated_at}. Message payloads stay out of the status API until the Turbostarter AI chat bridge lands.`
|
|
322
|
-
: "OpenCode session creation and continuation need an audited write route. This surface is ready for the Turbostarter AI adapter once connected."
|
|
323
|
-
}
|
|
324
|
-
/>
|
|
325
|
-
<AttachmentCard label="Context attachment" detail={selectedRepo ? `Selected repo context: ${selectedRepo.path_ref}` : "No local repos were discovered yet."} />
|
|
326
|
-
<MessageMarker label="Reasoning" detail="Reasoning disclosure, tool status, sources, retries, errors, and token usage render here when the AI transport supplies those parts." />
|
|
327
|
-
<ToolStatusCard />
|
|
328
|
-
<TamboConversationPart
|
|
329
|
-
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" }}
|
|
330
|
-
scope={{ tenant_ref: "local", workspace_ref: "aidevops", repo_ref: selectedRepo?.path_ref ?? null }}
|
|
331
|
-
/>
|
|
332
|
-
</div>
|
|
333
|
-
<form className="chat-composer ai-composer" aria-label="AI prompt composer" data-tour="ai-composer">
|
|
334
|
-
<button disabled title="Attachments need the audited upload/context adapter" type="button"><FiPaperclip aria-hidden="true" /> Attach</button>
|
|
335
|
-
<textarea disabled placeholder={text.chatInputPlaceholder} />
|
|
336
|
-
<button disabled title="Sending needs the Turbostarter AI transport adapter" type="button">Send</button>
|
|
337
|
-
</form>
|
|
338
|
-
</div>
|
|
339
|
-
</section>
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const sessionActions = ["New", "Rename", "Pin", "Archive", "Delete", "Share", "Export"] as const;
|
|
344
|
-
|
|
345
|
-
function sessionForRepo(status: GuiStatusData, repoPathRef: string, selectedSessionId: string | undefined) {
|
|
346
|
-
const sessions = status.opencode_sessions?.sessions ?? [];
|
|
347
|
-
return sessions.find((session) => session.id_ref === selectedSessionId && session.repo_path_ref === repoPathRef) ?? sessions.find((session) => session.repo_path_ref === repoPathRef);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function MessageMarker({ detail, label }: { detail: string; label: string }) {
|
|
351
|
-
return <article className="chat-bubble assistant"><strong>{label}</strong><p>{detail}</p></article>;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function AttachmentCard({ detail, label }: { detail: string; label: string }) {
|
|
355
|
-
return <article className="chat-bubble user"><strong><FiFileText aria-hidden="true" /> {label}</strong><p>{detail}</p></article>;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function ToolStatusCard() {
|
|
359
|
-
return <article className="chat-bubble assistant"><strong>Tool status</strong><p>Tool calls, source citations, retries, errors, and usage metadata are reserved for the shared conversation part model.</p></article>;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistant" | "user"; title: string }) {
|
|
363
|
-
return (
|
|
364
|
-
<article className={`chat-bubble ${speaker}`}>
|
|
365
|
-
<strong>{title}</strong>
|
|
366
|
-
<p>{body}</p>
|
|
367
|
-
</article>
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
266
|
function ProfileMenu({ onVaultRequest, openSurface, status, userName }: { onVaultRequest: (intent: VaultDialogIntent) => void; openSurface: (surface: SurfaceId) => void; status: GuiStatusData; userName: string }): ReactElement {
|
|
372
267
|
const vaultIntent = vaultDialogIntentForStatus(status.vault);
|
|
373
268
|
const VaultIcon = vaultIntent === "lock" ? FiLock : vaultIntent === "recover" || vaultIntent === "unavailable" ? FiAlertTriangle : FiUnlock;
|
|
@@ -512,74 +512,24 @@ export const plannedHomes = [
|
|
|
512
512
|
{ area: "Desktop wrapper", home: "Installation", phase: "P16" },
|
|
513
513
|
];
|
|
514
514
|
|
|
515
|
+
function inventoryConfig(title: string, intro: string, columns: Array<[key: string, label: string]>, initialRows: Record<string, string>[]): InventorySurfaceConfig {
|
|
516
|
+
return { title, intro, columns: columns.map(([key, label]) => ({ key, label })), initialRows };
|
|
517
|
+
}
|
|
518
|
+
|
|
515
519
|
export const inventorySurfaceConfigs: Partial<Record<SurfaceId, InventorySurfaceConfig>> = {
|
|
516
|
-
personas: {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
{ key: "brandName", label: "Brand Name" },
|
|
530
|
-
{ key: "website", label: "Website URL" },
|
|
531
|
-
],
|
|
532
|
-
initialRows: [{ brandName: "", website: "" }],
|
|
533
|
-
},
|
|
534
|
-
domains: {
|
|
535
|
-
title: text.domains,
|
|
536
|
-
intro: text.domainsIntro,
|
|
537
|
-
columns: [
|
|
538
|
-
{ key: "domain", label: "Domain" },
|
|
539
|
-
{ key: "provider", label: "Provider" },
|
|
540
|
-
{ key: "status", label: "Status" },
|
|
541
|
-
],
|
|
542
|
-
initialRows: [{ domain: "", provider: "", status: "" }],
|
|
543
|
-
},
|
|
544
|
-
registrars: {
|
|
545
|
-
title: text.registrars,
|
|
546
|
-
intro: text.registrarsIntro,
|
|
547
|
-
columns: [
|
|
548
|
-
{ key: "name", label: "Registrar" },
|
|
549
|
-
{ key: "recommendation", label: "Recommendation" },
|
|
550
|
-
{ key: "notes", label: "Notes" },
|
|
551
|
-
],
|
|
552
|
-
initialRows: [
|
|
553
|
-
{ name: "Porkbun", recommendation: "recommended", notes: "provider catalog" },
|
|
554
|
-
{ name: "Cloudflare Registrar", recommendation: "recommended", notes: "provider catalog" },
|
|
555
|
-
],
|
|
556
|
-
},
|
|
557
|
-
hosts: {
|
|
558
|
-
title: text.hosts,
|
|
559
|
-
intro: text.hostsIntro,
|
|
560
|
-
columns: [
|
|
561
|
-
{ key: "name", label: "Host" },
|
|
562
|
-
{ key: "category", label: "Category" },
|
|
563
|
-
{ key: "notes", label: "Notes" },
|
|
564
|
-
],
|
|
565
|
-
initialRows: [
|
|
566
|
-
{ name: "Cloudron", category: "app platform", notes: "recommended" },
|
|
567
|
-
{ name: "Coolify", category: "app platform", notes: "recommended" },
|
|
568
|
-
{ name: "Ubicloud", category: "cloud", notes: "recommended" },
|
|
569
|
-
],
|
|
570
|
-
},
|
|
571
|
-
servers: {
|
|
572
|
-
title: text.servers,
|
|
573
|
-
intro: text.serversIntro,
|
|
574
|
-
columns: [
|
|
575
|
-
{ key: "provider", label: "Provider" },
|
|
576
|
-
{ key: "server", label: "Server" },
|
|
577
|
-
{ key: "os", label: "OS" },
|
|
578
|
-
{ key: "orchestrator", label: "Orchestrator" },
|
|
579
|
-
{ key: "apps", label: "Apps" },
|
|
580
|
-
],
|
|
581
|
-
initialRows: [{ provider: "", server: "", os: "", orchestrator: "", apps: "" }],
|
|
582
|
-
},
|
|
520
|
+
personas: inventoryConfig(text.personas, text.personasIntro, [["firstName", "First Name"], ["lastName", "Last Name"]], [{ firstName: "", lastName: "" }]),
|
|
521
|
+
brands: inventoryConfig(text.brands, text.brandsIntro, [["brandName", "Brand Name"], ["website", "Website URL"]], [{ brandName: "", website: "" }]),
|
|
522
|
+
domains: inventoryConfig(text.domains, text.domainsIntro, [["domain", "Domain"], ["provider", "Provider"], ["status", "Status"]], [{ domain: "", provider: "", status: "" }]),
|
|
523
|
+
registrars: inventoryConfig(text.registrars, text.registrarsIntro, [["name", "Registrar"], ["recommendation", "Recommendation"], ["notes", "Notes"]], [
|
|
524
|
+
{ name: "Porkbun", recommendation: "recommended", notes: "provider catalog" },
|
|
525
|
+
{ name: "Cloudflare Registrar", recommendation: "recommended", notes: "provider catalog" },
|
|
526
|
+
]),
|
|
527
|
+
hosts: inventoryConfig(text.hosts, text.hostsIntro, [["name", "Host"], ["category", "Category"], ["notes", "Notes"]], [
|
|
528
|
+
{ name: "Cloudron", category: "app platform", notes: "recommended" },
|
|
529
|
+
{ name: "Coolify", category: "app platform", notes: "recommended" },
|
|
530
|
+
{ name: "Ubicloud", category: "cloud", notes: "recommended" },
|
|
531
|
+
]),
|
|
532
|
+
servers: inventoryConfig(text.servers, text.serversIntro, [["provider", "Provider"], ["server", "Server"], ["os", "OS"], ["orchestrator", "Orchestrator"], ["apps", "Apps"]], [{ provider: "", server: "", os: "", orchestrator: "", apps: "" }]),
|
|
583
533
|
};
|
|
584
534
|
|
|
585
535
|
export function getSystemTheme(): "light" | "dark" {
|
package/setup.sh
CHANGED
|
@@ -17,7 +17,7 @@ fi
|
|
|
17
17
|
# AI Assistant Server Access Framework Setup Script
|
|
18
18
|
# Helps developers set up the framework for their infrastructure
|
|
19
19
|
#
|
|
20
|
-
# Version: 3.39.
|
|
20
|
+
# Version: 3.39.57
|
|
21
21
|
#
|
|
22
22
|
# Quick Install:
|
|
23
23
|
# npm install -g aidevops && aidevops update (recommended)
|