@omercnet/paseo-context-mode 0.0.0
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/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.md +74 -0
- package/client/actions-section.tsx +621 -0
- package/client/analytics-dashboard.tsx +721 -0
- package/client/context-mode-pill.tsx +106 -0
- package/client/context-mode-surface.tsx +414 -0
- package/client/knowledge-section.tsx +780 -0
- package/client/settings-screen.tsx +226 -0
- package/index.client.tsx +43 -0
- package/index.server.ts +54 -0
- package/package.json +72 -0
- package/paseo-plugin.json +5 -0
- package/server/actions.ts +146 -0
- package/server/analytics.ts +424 -0
- package/server/binary.ts +182 -0
- package/server/context-mode-db.ts +481 -0
- package/server/handlers.ts +200 -0
- package/server/integration.ts +441 -0
- package/server/knowledge.ts +187 -0
- package/server/mcp-process.ts +357 -0
- package/server/storage-discovery.ts +225 -0
- package/shared/actions.ts +57 -0
- package/shared/analytics.ts +134 -0
- package/shared/contracts.ts +95 -0
- package/shared/index.ts +5 -0
- package/shared/knowledge.ts +150 -0
- package/shared/settings.ts +51 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { PluginButtonRegistration, PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
|
|
3
|
+
type PillAgent = {
|
|
4
|
+
id: string;
|
|
5
|
+
workspaceId?: string | null;
|
|
6
|
+
status: string;
|
|
7
|
+
archivedAt?: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type PillEntry = {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
registration: PluginButtonRegistration;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function shouldShowPill(agent: PillAgent): agent is PillAgent & { workspaceId: string } {
|
|
16
|
+
return Boolean(agent.workspaceId) && !agent.archivedAt && agent.status !== "closed";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Adds a neutral Context Mode status shortcut to every active agent composer. */
|
|
20
|
+
export function contributeContextModeComposerPills(client: PluginClientContext): () => void {
|
|
21
|
+
const pills = new Map<string, PillEntry>();
|
|
22
|
+
const lifetime = new AbortController();
|
|
23
|
+
let removeObserver: (() => void) | undefined;
|
|
24
|
+
let stopped = false;
|
|
25
|
+
|
|
26
|
+
function removePill(agentId: string): void {
|
|
27
|
+
pills.get(agentId)?.registration.remove();
|
|
28
|
+
pills.delete(agentId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function syncAgent(agent: PillAgent): void {
|
|
32
|
+
if (stopped) return;
|
|
33
|
+
if (!shouldShowPill(agent)) {
|
|
34
|
+
removePill(agent.id);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const current = pills.get(agent.id);
|
|
39
|
+
if (current?.workspaceId === agent.workspaceId) return;
|
|
40
|
+
removePill(agent.id);
|
|
41
|
+
|
|
42
|
+
const registration = client.addComposerPill({
|
|
43
|
+
id: "context-mode",
|
|
44
|
+
workspaceId: agent.workspaceId,
|
|
45
|
+
agentId: agent.id,
|
|
46
|
+
button: {
|
|
47
|
+
title: "View Context Mode status",
|
|
48
|
+
icon: "Gauge",
|
|
49
|
+
label: "Context Mode",
|
|
50
|
+
behavior: {
|
|
51
|
+
kind: "action",
|
|
52
|
+
onPress() {
|
|
53
|
+
client.openSurface("context-mode");
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
pills.set(agent.id, { workspaceId: agent.workspaceId, registration });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function syncSnapshot(entries: readonly { agent: PillAgent }[]): void {
|
|
62
|
+
if (stopped) return;
|
|
63
|
+
const observedAgentIds = new Set<string>();
|
|
64
|
+
for (const { agent } of entries) {
|
|
65
|
+
if (shouldShowPill(agent)) observedAgentIds.add(agent.id);
|
|
66
|
+
syncAgent(agent);
|
|
67
|
+
}
|
|
68
|
+
for (const agentId of pills.keys()) {
|
|
69
|
+
if (!observedAgentIds.has(agentId)) removePill(agentId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void client.paseo.agents
|
|
74
|
+
.list({
|
|
75
|
+
filter: { includeArchived: false },
|
|
76
|
+
subscribe: {},
|
|
77
|
+
signal: lifetime.signal,
|
|
78
|
+
})
|
|
79
|
+
.then(({ subscription }) => {
|
|
80
|
+
if (stopped) {
|
|
81
|
+
return subscription.release();
|
|
82
|
+
}
|
|
83
|
+
removeObserver = subscription.subscribe({
|
|
84
|
+
snapshot: ({ entries }) => syncSnapshot(entries),
|
|
85
|
+
update: (message) => {
|
|
86
|
+
if (message.type !== "agent_update") return;
|
|
87
|
+
const update = message.payload;
|
|
88
|
+
if (update.kind === "remove") removePill(update.agentId);
|
|
89
|
+
else syncAgent(update.agent);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return undefined;
|
|
93
|
+
})
|
|
94
|
+
.catch((error: unknown) => {
|
|
95
|
+
if (!stopped) console.error("[context-mode] Agent observation failed", error);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
if (stopped) return;
|
|
100
|
+
stopped = true;
|
|
101
|
+
removeObserver?.();
|
|
102
|
+
lifetime.abort();
|
|
103
|
+
for (const { registration } of pills.values()) registration.remove();
|
|
104
|
+
pills.clear();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
|
6
|
+
import {
|
|
7
|
+
type BinaryStatus,
|
|
8
|
+
contextModeSettings,
|
|
9
|
+
getContextModeIntegrationAudit,
|
|
10
|
+
getContextModeStats,
|
|
11
|
+
getContextModeStatus,
|
|
12
|
+
type IntegrationAudit,
|
|
13
|
+
} from "../shared";
|
|
14
|
+
import { ActionsSection } from "./actions-section";
|
|
15
|
+
import { AnalyticsDashboard } from "./analytics-dashboard";
|
|
16
|
+
import { KnowledgeSection } from "./knowledge-section";
|
|
17
|
+
|
|
18
|
+
interface ContextModeSurfaceProps extends PluginSurfaceProps {
|
|
19
|
+
onOpenSettings(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function statusLabel(status: BinaryStatus): string {
|
|
23
|
+
if (status.state === "ready") return "Connected";
|
|
24
|
+
if (status.code === "not-installed") return "Not installed";
|
|
25
|
+
if (status.code === "unsupported") return "Unsupported";
|
|
26
|
+
return "Unavailable";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type SurfaceTab = "savings" | "knowledge" | "setup";
|
|
30
|
+
|
|
31
|
+
const SURFACE_TABS: ReadonlyArray<{ id: SurfaceTab; label: string }> = [
|
|
32
|
+
{ id: "savings", label: "Savings" },
|
|
33
|
+
{ id: "knowledge", label: "Knowledge" },
|
|
34
|
+
{ id: "setup", label: "Setup" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function ContextModeSurface({
|
|
38
|
+
theme,
|
|
39
|
+
host,
|
|
40
|
+
layout,
|
|
41
|
+
onOpenSettings,
|
|
42
|
+
}: ContextModeSurfaceProps) {
|
|
43
|
+
const settings = useSettings(contextModeSettings);
|
|
44
|
+
const loadStatus = useRpc(getContextModeStatus);
|
|
45
|
+
const loadStats = useRpc(getContextModeStats);
|
|
46
|
+
const loadIntegrationAudit = useRpc(getContextModeIntegrationAudit);
|
|
47
|
+
const queryClient = useQueryClient();
|
|
48
|
+
const [activeTab, setActiveTab] = useState<SurfaceTab>("savings");
|
|
49
|
+
const readySettings = settings.status === "ready" ? settings : null;
|
|
50
|
+
const refreshInterval = readySettings?.values.refreshIntervalMs ?? 15_000;
|
|
51
|
+
const statusKey = ["context-mode", host.id, "status"] as const;
|
|
52
|
+
const statsKey = ["context-mode", host.id, "stats"] as const;
|
|
53
|
+
const integrationKey = ["context-mode", host.id, "integration"] as const;
|
|
54
|
+
const status = useQuery({
|
|
55
|
+
queryKey: statusKey,
|
|
56
|
+
queryFn: () => loadStatus({ fresh: false }),
|
|
57
|
+
refetchInterval: refreshInterval,
|
|
58
|
+
enabled: readySettings !== null,
|
|
59
|
+
});
|
|
60
|
+
const stats = useQuery({
|
|
61
|
+
queryKey: statsKey,
|
|
62
|
+
queryFn: () => loadStats({ fresh: false }),
|
|
63
|
+
refetchInterval: refreshInterval,
|
|
64
|
+
enabled: readySettings !== null && status.data?.state === "ready",
|
|
65
|
+
});
|
|
66
|
+
const integration = useQuery({
|
|
67
|
+
queryKey: integrationKey,
|
|
68
|
+
queryFn: () => loadIntegrationAudit({ fresh: false }),
|
|
69
|
+
refetchInterval: refreshInterval,
|
|
70
|
+
enabled: readySettings !== null,
|
|
71
|
+
});
|
|
72
|
+
const refresh = useMutation({
|
|
73
|
+
mutationFn: async () => {
|
|
74
|
+
const nextStatus = await loadStatus({ fresh: true });
|
|
75
|
+
const nextIntegration = await loadIntegrationAudit({ fresh: true });
|
|
76
|
+
const nextStats =
|
|
77
|
+
nextStatus.state === "ready" && nextStatus.supportsStats
|
|
78
|
+
? await loadStats({ fresh: true })
|
|
79
|
+
: undefined;
|
|
80
|
+
return { nextStatus, nextIntegration, nextStats };
|
|
81
|
+
},
|
|
82
|
+
onSuccess({ nextStatus, nextIntegration, nextStats }) {
|
|
83
|
+
queryClient.setQueryData(statusKey, nextStatus);
|
|
84
|
+
queryClient.setQueryData(integrationKey, nextIntegration);
|
|
85
|
+
if (nextStats) queryClient.setQueryData(statsKey, nextStats);
|
|
86
|
+
void queryClient.invalidateQueries({
|
|
87
|
+
queryKey: ["context-mode", host.id, "analytics-dashboard"],
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const styles = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
screen: {
|
|
94
|
+
flex: 1,
|
|
95
|
+
backgroundColor: theme.colors.surface0,
|
|
96
|
+
},
|
|
97
|
+
content: {
|
|
98
|
+
padding: layout.compact ? 14 : 24,
|
|
99
|
+
gap: layout.compact ? 14 : 20,
|
|
100
|
+
width: "100%" as const,
|
|
101
|
+
maxWidth: 920,
|
|
102
|
+
alignSelf: "center" as const,
|
|
103
|
+
},
|
|
104
|
+
header: { flexDirection: "row" as const, alignItems: "center" as const, gap: 12 },
|
|
105
|
+
headerCopy: { flex: 1, gap: 2 },
|
|
106
|
+
title: {
|
|
107
|
+
color: theme.colors.foreground,
|
|
108
|
+
fontSize: layout.compact ? 22 : 28,
|
|
109
|
+
fontWeight: "700" as const,
|
|
110
|
+
},
|
|
111
|
+
subtitle: { color: theme.colors.foregroundMuted, fontSize: 13 },
|
|
112
|
+
section: { gap: 8 },
|
|
113
|
+
sectionTitle: {
|
|
114
|
+
color: theme.colors.foreground,
|
|
115
|
+
fontSize: 15,
|
|
116
|
+
fontWeight: "700" as const,
|
|
117
|
+
},
|
|
118
|
+
card: {
|
|
119
|
+
gap: 10,
|
|
120
|
+
borderWidth: 1,
|
|
121
|
+
borderColor: theme.colors.border,
|
|
122
|
+
borderRadius: 12,
|
|
123
|
+
padding: layout.compact ? 12 : 16,
|
|
124
|
+
backgroundColor: theme.colors.surface1,
|
|
125
|
+
},
|
|
126
|
+
row: { flexDirection: "row" as const, alignItems: "center" as const, gap: 8 },
|
|
127
|
+
grow: { flex: 1 },
|
|
128
|
+
label: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
129
|
+
value: { color: theme.colors.foreground, fontSize: 14 },
|
|
130
|
+
success: { color: theme.colors.statusSuccess, fontWeight: "600" as const },
|
|
131
|
+
warning: { color: theme.colors.statusWarning, fontWeight: "600" as const },
|
|
132
|
+
error: { color: theme.colors.statusDanger },
|
|
133
|
+
output: {
|
|
134
|
+
color: theme.colors.foreground,
|
|
135
|
+
fontFamily: "monospace",
|
|
136
|
+
fontSize: layout.compact ? 11 : 12,
|
|
137
|
+
lineHeight: layout.compact ? 16 : 18,
|
|
138
|
+
},
|
|
139
|
+
button: {
|
|
140
|
+
minHeight: 36,
|
|
141
|
+
paddingHorizontal: 12,
|
|
142
|
+
paddingVertical: 8,
|
|
143
|
+
borderRadius: 8,
|
|
144
|
+
backgroundColor: theme.colors.surface2,
|
|
145
|
+
alignItems: "center" as const,
|
|
146
|
+
justifyContent: "center" as const,
|
|
147
|
+
},
|
|
148
|
+
primaryButton: { backgroundColor: theme.colors.accent },
|
|
149
|
+
buttonText: { color: theme.colors.foreground, fontWeight: "600" as const },
|
|
150
|
+
primaryButtonText: { color: theme.colors.accentForeground, fontWeight: "600" as const },
|
|
151
|
+
actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 8 },
|
|
152
|
+
healthLine: {
|
|
153
|
+
flexDirection: "row" as const,
|
|
154
|
+
alignItems: "center" as const,
|
|
155
|
+
flexWrap: "wrap" as const,
|
|
156
|
+
gap: 8,
|
|
157
|
+
},
|
|
158
|
+
chip: {
|
|
159
|
+
borderRadius: 999,
|
|
160
|
+
paddingHorizontal: 9,
|
|
161
|
+
paddingVertical: 5,
|
|
162
|
+
backgroundColor: theme.colors.surface2,
|
|
163
|
+
},
|
|
164
|
+
chipText: { color: theme.colors.foreground, fontSize: 11, fontWeight: "600" as const },
|
|
165
|
+
tabRow: {
|
|
166
|
+
flexDirection: "row" as const,
|
|
167
|
+
gap: 6,
|
|
168
|
+
padding: 4,
|
|
169
|
+
borderRadius: 10,
|
|
170
|
+
backgroundColor: theme.colors.surface1,
|
|
171
|
+
},
|
|
172
|
+
tab: {
|
|
173
|
+
flex: 1,
|
|
174
|
+
minHeight: 38,
|
|
175
|
+
alignItems: "center" as const,
|
|
176
|
+
justifyContent: "center" as const,
|
|
177
|
+
borderRadius: 8,
|
|
178
|
+
paddingHorizontal: 10,
|
|
179
|
+
},
|
|
180
|
+
tabActive: { backgroundColor: theme.colors.surface2 },
|
|
181
|
+
tabText: { color: theme.colors.foregroundMuted, fontWeight: "600" as const },
|
|
182
|
+
tabTextActive: { color: theme.colors.foreground },
|
|
183
|
+
tabPanel: { width: "100%" as const },
|
|
184
|
+
tabPanelHidden: { display: "none" as const },
|
|
185
|
+
}),
|
|
186
|
+
[layout.compact, theme],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (settings.status === "loading") {
|
|
190
|
+
return (
|
|
191
|
+
<View style={[styles.screen, { alignItems: "center", justifyContent: "center" }]}>
|
|
192
|
+
<ActivityIndicator color={theme.colors.accent} />
|
|
193
|
+
<Text style={styles.subtitle}>Loading Context Mode settings…</Text>
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (settings.status !== "ready") {
|
|
199
|
+
return (
|
|
200
|
+
<View style={[styles.screen, styles.content]}>
|
|
201
|
+
<Text style={styles.title}>Context Mode</Text>
|
|
202
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
203
|
+
{settings.error}
|
|
204
|
+
</Text>
|
|
205
|
+
<Pressable accessibilityRole="button" onPress={settings.reload} style={styles.button}>
|
|
206
|
+
<Text style={styles.buttonText}>Reload settings</Text>
|
|
207
|
+
</Pressable>
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const statusData = status.data;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<ScrollView style={styles.screen} contentContainerStyle={styles.content}>
|
|
216
|
+
<View style={styles.header}>
|
|
217
|
+
<Icon name="Gauge" size={layout.compact ? 24 : 30} color={theme.colors.accent} />
|
|
218
|
+
<View style={styles.headerCopy}>
|
|
219
|
+
<Text style={styles.title}>Context Mode</Text>
|
|
220
|
+
<Text style={styles.subtitle}>Context saved across your agents on {host.label}</Text>
|
|
221
|
+
</View>
|
|
222
|
+
</View>
|
|
223
|
+
|
|
224
|
+
<AnalyticsDashboard
|
|
225
|
+
display="hero"
|
|
226
|
+
fallback={stats.data?.state === "ready" ? stats.data.output : null}
|
|
227
|
+
health={
|
|
228
|
+
<HealthStrip
|
|
229
|
+
audit={integration.data}
|
|
230
|
+
auditError={integration.error}
|
|
231
|
+
auditPending={integration.isPending}
|
|
232
|
+
onOpenSettings={onOpenSettings}
|
|
233
|
+
onRefresh={() => refresh.mutate()}
|
|
234
|
+
refreshError={refresh.error}
|
|
235
|
+
refreshPending={refresh.isPending}
|
|
236
|
+
status={statusData}
|
|
237
|
+
statusError={status.error}
|
|
238
|
+
statusPending={status.isPending}
|
|
239
|
+
styles={styles}
|
|
240
|
+
/>
|
|
241
|
+
}
|
|
242
|
+
host={host}
|
|
243
|
+
layout={layout}
|
|
244
|
+
theme={theme}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
<View accessibilityRole="tablist" style={styles.tabRow}>
|
|
248
|
+
{SURFACE_TABS.map((tab) => {
|
|
249
|
+
const selected = activeTab === tab.id;
|
|
250
|
+
return (
|
|
251
|
+
<Pressable
|
|
252
|
+
key={tab.id}
|
|
253
|
+
accessibilityRole="tab"
|
|
254
|
+
accessibilityState={{ selected }}
|
|
255
|
+
onPress={() => setActiveTab(tab.id)}
|
|
256
|
+
style={[styles.tab, selected ? styles.tabActive : null]}
|
|
257
|
+
>
|
|
258
|
+
<Text style={[styles.tabText, selected ? styles.tabTextActive : null]}>
|
|
259
|
+
{tab.label}
|
|
260
|
+
</Text>
|
|
261
|
+
</Pressable>
|
|
262
|
+
);
|
|
263
|
+
})}
|
|
264
|
+
</View>
|
|
265
|
+
|
|
266
|
+
<View style={[styles.tabPanel, activeTab === "savings" ? null : styles.tabPanelHidden]}>
|
|
267
|
+
<AnalyticsDashboard
|
|
268
|
+
display="details"
|
|
269
|
+
fallback={stats.data?.state === "ready" ? stats.data.output : null}
|
|
270
|
+
host={host}
|
|
271
|
+
layout={layout}
|
|
272
|
+
theme={theme}
|
|
273
|
+
/>
|
|
274
|
+
</View>
|
|
275
|
+
<View style={[styles.tabPanel, activeTab === "knowledge" ? null : styles.tabPanelHidden]}>
|
|
276
|
+
<KnowledgeSection key={host.id} host={host} layout={layout} theme={theme} />
|
|
277
|
+
</View>
|
|
278
|
+
<View style={[styles.tabPanel, activeTab === "setup" ? null : styles.tabPanelHidden]}>
|
|
279
|
+
<ActionsSection key={host.id} layout={layout} theme={theme} />
|
|
280
|
+
</View>
|
|
281
|
+
</ScrollView>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function HealthStrip({
|
|
286
|
+
audit,
|
|
287
|
+
auditError,
|
|
288
|
+
auditPending,
|
|
289
|
+
onOpenSettings,
|
|
290
|
+
onRefresh,
|
|
291
|
+
refreshError,
|
|
292
|
+
refreshPending,
|
|
293
|
+
status,
|
|
294
|
+
statusError,
|
|
295
|
+
statusPending,
|
|
296
|
+
styles,
|
|
297
|
+
}: {
|
|
298
|
+
audit: IntegrationAudit | undefined;
|
|
299
|
+
auditError: unknown;
|
|
300
|
+
auditPending: boolean;
|
|
301
|
+
onOpenSettings(): void;
|
|
302
|
+
onRefresh(): void;
|
|
303
|
+
refreshError: unknown;
|
|
304
|
+
refreshPending: boolean;
|
|
305
|
+
status: BinaryStatus | undefined;
|
|
306
|
+
statusError: unknown;
|
|
307
|
+
statusPending: boolean;
|
|
308
|
+
styles: {
|
|
309
|
+
actions: object;
|
|
310
|
+
button: object;
|
|
311
|
+
buttonText: object;
|
|
312
|
+
card: object;
|
|
313
|
+
chip: object;
|
|
314
|
+
chipText: object;
|
|
315
|
+
error: object;
|
|
316
|
+
grow: object;
|
|
317
|
+
healthLine: object;
|
|
318
|
+
label: object;
|
|
319
|
+
success: object;
|
|
320
|
+
subtitle: object;
|
|
321
|
+
value: object;
|
|
322
|
+
warning: object;
|
|
323
|
+
};
|
|
324
|
+
}) {
|
|
325
|
+
const [expanded, setExpanded] = useState(false);
|
|
326
|
+
const covered =
|
|
327
|
+
audit?.providers.filter((provider) => provider.activation !== "disabled").length ?? 0;
|
|
328
|
+
const total = audit?.providers.length ?? 0;
|
|
329
|
+
const nativeCount =
|
|
330
|
+
audit?.providers.filter((provider) => provider.activation === "native").length ?? 0;
|
|
331
|
+
const mcpCount = audit?.providers.filter((provider) => provider.activation === "mcp").length ?? 0;
|
|
332
|
+
const statusReady = status?.state === "ready";
|
|
333
|
+
const error = statusError ?? auditError ?? refreshError;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<View style={styles.card}>
|
|
337
|
+
<View style={styles.healthLine}>
|
|
338
|
+
{statusPending || auditPending ? <ActivityIndicator size="small" /> : null}
|
|
339
|
+
<Text style={statusReady ? styles.success : styles.warning}>
|
|
340
|
+
{status ? statusLabel(status) : "Checking"}
|
|
341
|
+
</Text>
|
|
342
|
+
{statusReady && status.version ? <Text style={styles.value}>v{status.version}</Text> : null}
|
|
343
|
+
<View style={styles.grow} />
|
|
344
|
+
<Text style={styles.value}>
|
|
345
|
+
{total > 0 ? `${covered} of ${total} providers covered` : "Coverage unavailable"}
|
|
346
|
+
</Text>
|
|
347
|
+
</View>
|
|
348
|
+
|
|
349
|
+
{status?.state === "unavailable" ? (
|
|
350
|
+
<Text style={styles.subtitle}>{status.message}</Text>
|
|
351
|
+
) : null}
|
|
352
|
+
{error ? (
|
|
353
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
354
|
+
{error instanceof Error ? error.message : String(error)}
|
|
355
|
+
</Text>
|
|
356
|
+
) : null}
|
|
357
|
+
{statusReady && (!status.supportsDoctor || !status.supportsStats) ? (
|
|
358
|
+
<Text style={styles.warning}>
|
|
359
|
+
Some Context Mode health or savings tools are unavailable.
|
|
360
|
+
</Text>
|
|
361
|
+
) : null}
|
|
362
|
+
|
|
363
|
+
<View style={styles.actions}>
|
|
364
|
+
<Pressable
|
|
365
|
+
accessibilityRole="button"
|
|
366
|
+
accessibilityState={{ expanded }}
|
|
367
|
+
onPress={() => setExpanded((value) => !value)}
|
|
368
|
+
style={styles.button}
|
|
369
|
+
>
|
|
370
|
+
<Text style={styles.buttonText}>{expanded ? "Hide coverage" : "Coverage details"}</Text>
|
|
371
|
+
</Pressable>
|
|
372
|
+
<Pressable
|
|
373
|
+
accessibilityRole="button"
|
|
374
|
+
disabled={refreshPending}
|
|
375
|
+
onPress={onRefresh}
|
|
376
|
+
style={styles.button}
|
|
377
|
+
>
|
|
378
|
+
<Text style={styles.buttonText}>{refreshPending ? "Refreshing…" : "Refresh"}</Text>
|
|
379
|
+
</Pressable>
|
|
380
|
+
<Pressable accessibilityRole="button" onPress={onOpenSettings} style={styles.button}>
|
|
381
|
+
<Text style={styles.buttonText}>Settings</Text>
|
|
382
|
+
</Pressable>
|
|
383
|
+
</View>
|
|
384
|
+
|
|
385
|
+
{expanded && audit ? (
|
|
386
|
+
<View style={{ gap: 8 }}>
|
|
387
|
+
{mcpCount > 0 ? (
|
|
388
|
+
<Text style={styles.subtitle}>
|
|
389
|
+
MCP coverage applies to new agents. Existing agents may need recreation or manual
|
|
390
|
+
setup.
|
|
391
|
+
</Text>
|
|
392
|
+
) : null}
|
|
393
|
+
<View style={styles.actions}>
|
|
394
|
+
{audit.providers.map((provider) => (
|
|
395
|
+
<View key={provider.provider} style={styles.chip}>
|
|
396
|
+
<Text style={styles.chipText}>
|
|
397
|
+
{provider.provider} ·{" "}
|
|
398
|
+
{provider.activation === "native"
|
|
399
|
+
? "Native"
|
|
400
|
+
: provider.activation === "mcp"
|
|
401
|
+
? "MCP · new agents"
|
|
402
|
+
: "Off"}
|
|
403
|
+
</Text>
|
|
404
|
+
</View>
|
|
405
|
+
))}
|
|
406
|
+
</View>
|
|
407
|
+
<Text style={styles.label}>
|
|
408
|
+
{nativeCount} native · {mcpCount} MCP · {total - covered} off
|
|
409
|
+
</Text>
|
|
410
|
+
</View>
|
|
411
|
+
) : null}
|
|
412
|
+
</View>
|
|
413
|
+
);
|
|
414
|
+
}
|