@omercnet/paseo-context-mode 3.1.3 → 3.1.4-next.434.1

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.
@@ -0,0 +1,377 @@
1
+ import type { PluginTheme } from "@getpaseo/plugin";
2
+ import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
3
+ import { Icon, Loading, ScrollView, useToast } from "@getpaseo/plugin/client/ui";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useState } from "react";
6
+ import { Text, View } from "react-native";
7
+ import {
8
+ type BinaryStatus,
9
+ getContextModeIntegrationAudit,
10
+ getContextModeStatus,
11
+ type IntegrationAudit,
12
+ type ProviderIntegration,
13
+ } from "../shared/contracts";
14
+ import { contextModeSettings } from "../shared/settings";
15
+ import { AnalyticsDashboard } from "./analytics-dashboard";
16
+ import {
17
+ ActionButton,
18
+ Card,
19
+ type ChoiceOption,
20
+ Choices,
21
+ ErrorText,
22
+ Pending,
23
+ StatusPill,
24
+ type StatusTone,
25
+ } from "./controls";
26
+ import { DiagnosticsSection } from "./diagnostics-section";
27
+ import { BUILTIN_KNOWLEDGE_PROVIDERS, KnowledgeSection, providerLabel } from "./knowledge-section";
28
+ import { ScopedKnowledgeSection } from "./scoped-knowledge-section";
29
+ import { parseScreenParams, type ScreenParams, type SurfaceTab } from "./screen-scope";
30
+
31
+ interface ContextModeSurfaceProps extends PluginSurfaceProps {
32
+ onOpenSettings(): void;
33
+ params?: Record<string, string>;
34
+ }
35
+
36
+ const SURFACE_TABS: readonly ChoiceOption<SurfaceTab>[] = [
37
+ { value: "savings", label: "Savings" },
38
+ { value: "knowledge", label: "Knowledge" },
39
+ { value: "setup", label: "Diagnostics" },
40
+ ];
41
+
42
+ interface Presentation {
43
+ label: string;
44
+ tone: StatusTone;
45
+ }
46
+
47
+ const ACTIVATION: Record<ProviderIntegration["activation"], Presentation> = {
48
+ native: { label: "Native", tone: "success" },
49
+ mcp: { label: "MCP, new agents", tone: "success" },
50
+ disabled: { label: "Off", tone: "neutral" },
51
+ };
52
+
53
+ const UNSUPPORTED: Presentation = { label: "Unsupported", tone: "neutral" };
54
+
55
+ function statusPresentation(status: BinaryStatus): Presentation {
56
+ if (status.state === "ready") return { label: "Connected", tone: "success" };
57
+ if (status.code === "not-installed") return { label: "Not installed", tone: "warning" };
58
+ if (status.code === "unsupported") return { label: "Unsupported", tone: "warning" };
59
+ return { label: "Unavailable", tone: "warning" };
60
+ }
61
+
62
+ /** Providers the panel can search and index for: those Context Mode serves over MCP. */
63
+ function supportedProviders(audit: IntegrationAudit | undefined): ProviderIntegration[] {
64
+ if (!audit) return [];
65
+ return audit.providers.filter(({ platform }) => platform !== null);
66
+ }
67
+
68
+ /** Coverage also counts hosts running their own Context Mode, like Pi's extension. */
69
+ function coveredProviders(audit: IntegrationAudit | undefined): ProviderIntegration[] {
70
+ if (!audit) return [];
71
+ return audit.providers.filter(
72
+ ({ platform, activation }) => platform !== null || activation === "native",
73
+ );
74
+ }
75
+
76
+ export function ContextModeSurface({
77
+ theme,
78
+ host,
79
+ layout,
80
+ onOpenSettings,
81
+ params,
82
+ }: ContextModeSurfaceProps) {
83
+ const settings = useSettings(contextModeSettings);
84
+ const loadStatus = useRpc(getContextModeStatus);
85
+ const loadIntegrationAudit = useRpc(getContextModeIntegrationAudit);
86
+ const queryClient = useQueryClient();
87
+ const toast = useToast();
88
+ const screenParams = parseScreenParams(params ?? {});
89
+ const settingsReady = settings.status === "ready";
90
+ const refreshInterval = settingsReady ? settings.values.refreshIntervalMs : 15_000;
91
+ const statusKey = ["context-mode", host.id, "status"] as const;
92
+ const integrationKey = ["context-mode", host.id, "integration"] as const;
93
+ const status = useQuery({
94
+ queryKey: statusKey,
95
+ queryFn: () => loadStatus({ fresh: false }),
96
+ refetchInterval: refreshInterval,
97
+ enabled: settingsReady,
98
+ });
99
+ const integration = useQuery({
100
+ queryKey: integrationKey,
101
+ queryFn: () => loadIntegrationAudit({}),
102
+ refetchInterval: refreshInterval,
103
+ enabled: settingsReady,
104
+ });
105
+ const refresh = useMutation({
106
+ mutationFn: async () => {
107
+ const nextStatus = await loadStatus({ fresh: true });
108
+ const nextIntegration = await loadIntegrationAudit({});
109
+ return { nextStatus, nextIntegration };
110
+ },
111
+ onSuccess({ nextStatus, nextIntegration }) {
112
+ queryClient.setQueryData(statusKey, nextStatus);
113
+ queryClient.setQueryData(integrationKey, nextIntegration);
114
+ void queryClient.invalidateQueries({
115
+ queryKey: ["context-mode", host.id, "analytics-dashboard"],
116
+ });
117
+ toast.show("Context Mode status refreshed", { variant: "success" });
118
+ },
119
+ });
120
+ const contentStyle = {
121
+ padding: layout.compact ? 16 : 24,
122
+ gap: layout.compact ? 16 : 24,
123
+ width: "100%" as const,
124
+ maxWidth: 920,
125
+ alignSelf: "center" as const,
126
+ };
127
+ const titleStyle = { color: theme.colors.foreground, fontSize: 20, fontWeight: "500" as const };
128
+
129
+ if (settings.status === "loading") return <Loading />;
130
+
131
+ if (settings.status !== "ready") {
132
+ return (
133
+ <View style={[{ flex: 1, backgroundColor: theme.colors.surface0 }, contentStyle]}>
134
+ <Text style={titleStyle}>Context Mode</Text>
135
+ <ErrorText theme={theme} error={settings.error} />
136
+ <ActionButton theme={theme} label="Reload settings" onPress={settings.reload} />
137
+ </View>
138
+ );
139
+ }
140
+
141
+ const supported = supportedProviders(integration.data).map(({ provider }) => provider);
142
+ const knowledgeProviders = supported.length > 0 ? supported : BUILTIN_KNOWLEDGE_PROVIDERS;
143
+ const paramsKey = JSON.stringify([
144
+ host.id,
145
+ screenParams.section,
146
+ screenParams.agentId,
147
+ screenParams.workspaceId,
148
+ ]);
149
+
150
+ return (
151
+ <ScrollView
152
+ style={{ flex: 1, backgroundColor: theme.colors.surface0 }}
153
+ contentContainerStyle={contentStyle}
154
+ >
155
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
156
+ <Icon name="Gauge" size={24} color={theme.colors.accent} />
157
+ <View style={{ flex: 1, gap: 4 }}>
158
+ <Text style={titleStyle}>Context Mode</Text>
159
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 13 }}>
160
+ Context saved across your agents on {host.label}
161
+ </Text>
162
+ </View>
163
+ </View>
164
+
165
+ <AnalyticsDashboard
166
+ display="hero"
167
+ health={
168
+ <HealthStrip
169
+ theme={theme}
170
+ audit={integration.data}
171
+ status={status.data}
172
+ pending={status.isPending || integration.isPending}
173
+ error={status.error ?? integration.error ?? refresh.error}
174
+ refreshing={refresh.isPending}
175
+ onRefresh={() => refresh.mutate()}
176
+ onOpenSettings={onOpenSettings}
177
+ />
178
+ }
179
+ host={host}
180
+ layout={layout}
181
+ theme={theme}
182
+ />
183
+
184
+ {/* Keyed by the params so opening the screen with new params selects their section again. */}
185
+ <SurfaceTabs
186
+ key={paramsKey}
187
+ host={host}
188
+ layout={layout}
189
+ theme={theme}
190
+ screenParams={screenParams}
191
+ providers={knowledgeProviders}
192
+ providersLoading={integration.isLoading}
193
+ />
194
+ </ScrollView>
195
+ );
196
+ }
197
+
198
+ interface SurfaceTabsProps extends Pick<PluginSurfaceProps, "host" | "layout" | "theme"> {
199
+ screenParams: ScreenParams;
200
+ providers: readonly string[];
201
+ providersLoading: boolean;
202
+ }
203
+
204
+ function SurfaceTabs({
205
+ host,
206
+ layout,
207
+ theme,
208
+ screenParams,
209
+ providers,
210
+ providersLoading,
211
+ }: SurfaceTabsProps) {
212
+ const [activeTab, setActiveTab] = useState<SurfaceTab>(screenParams.section);
213
+ const hidden = { display: "none" as const };
214
+ return (
215
+ <>
216
+ <Choices<SurfaceTab>
217
+ theme={theme}
218
+ role="tab"
219
+ options={SURFACE_TABS}
220
+ value={activeTab}
221
+ onChange={setActiveTab}
222
+ accessibilityLabel={(tab) => tab.label}
223
+ />
224
+ <View style={activeTab === "savings" ? null : hidden}>
225
+ <AnalyticsDashboard display="details" host={host} layout={layout} theme={theme} />
226
+ </View>
227
+ {activeTab === "knowledge" ? (
228
+ <KnowledgePanel
229
+ host={host}
230
+ layout={layout}
231
+ theme={theme}
232
+ screenParams={screenParams}
233
+ providers={providers}
234
+ providersLoading={providersLoading}
235
+ />
236
+ ) : null}
237
+ <View style={activeTab === "setup" ? null : hidden}>
238
+ <DiagnosticsSection key={host.id} theme={theme} />
239
+ </View>
240
+ </>
241
+ );
242
+ }
243
+
244
+ function KnowledgePanel({
245
+ host,
246
+ theme,
247
+ screenParams,
248
+ providers,
249
+ providersLoading,
250
+ }: SurfaceTabsProps) {
251
+ if (providersLoading) return <Pending theme={theme} label="Loading providers..." />;
252
+ if (screenParams.agentId || screenParams.workspaceId) {
253
+ return (
254
+ <ScopedKnowledgeSection
255
+ params={screenParams}
256
+ host={host}
257
+ theme={theme}
258
+ providers={providers}
259
+ />
260
+ );
261
+ }
262
+ return <KnowledgeSection key={host.id} host={host} theme={theme} providers={providers} />;
263
+ }
264
+
265
+ function coverageSummary(audit: IntegrationAudit | undefined): string {
266
+ const supported = coveredProviders(audit);
267
+ if (supported.length === 0) return "Coverage unavailable";
268
+ const covered = supported.filter(({ activation }) => activation !== "disabled").length;
269
+ return `${covered} of ${supported.length} providers covered`;
270
+ }
271
+
272
+ interface HealthStripProps {
273
+ theme: PluginTheme;
274
+ audit: IntegrationAudit | undefined;
275
+ status: BinaryStatus | undefined;
276
+ pending: boolean;
277
+ error: unknown;
278
+ refreshing: boolean;
279
+ onRefresh(): void;
280
+ onOpenSettings(): void;
281
+ }
282
+
283
+ function HealthStrip({
284
+ theme,
285
+ audit,
286
+ status,
287
+ pending,
288
+ error,
289
+ refreshing,
290
+ onRefresh,
291
+ onOpenSettings,
292
+ }: HealthStripProps) {
293
+ const [expanded, setExpanded] = useState(false);
294
+ const supported = coveredProviders(audit);
295
+ const nativeCount = supported.filter(({ activation }) => activation === "native").length;
296
+ const mcpCount = supported.filter(({ activation }) => activation === "mcp").length;
297
+ const offCount = supported.length - nativeCount - mcpCount;
298
+ const presentation = status ? statusPresentation(status) : null;
299
+ const version = status?.state === "ready" ? status.version : null;
300
+ const valueStyle = { color: theme.colors.foreground, fontSize: 14 };
301
+ const mutedStyle = { color: theme.colors.foregroundMuted, fontSize: 13 };
302
+ const rowStyle = {
303
+ flexDirection: "row" as const,
304
+ alignItems: "center" as const,
305
+ flexWrap: "wrap" as const,
306
+ gap: 8,
307
+ };
308
+
309
+ return (
310
+ <Card theme={theme}>
311
+ <View style={rowStyle}>
312
+ {pending ? <Pending theme={theme} label="Checking..." /> : null}
313
+ {presentation ? (
314
+ <StatusPill theme={theme} label={presentation.label} tone={presentation.tone} />
315
+ ) : null}
316
+ {version ? <Text style={valueStyle}>v{version}</Text> : null}
317
+ <View style={{ flex: 1 }} />
318
+ <Text style={valueStyle}>{coverageSummary(audit)}</Text>
319
+ </View>
320
+
321
+ {status?.state === "unavailable" ? <Text style={mutedStyle}>{status.message}</Text> : null}
322
+ <ErrorText theme={theme} error={error} />
323
+
324
+ <View style={rowStyle}>
325
+ <ActionButton
326
+ theme={theme}
327
+ expanded={expanded}
328
+ label={expanded ? "Hide coverage" : "Coverage details"}
329
+ onPress={() => setExpanded((value) => !value)}
330
+ />
331
+ <ActionButton
332
+ theme={theme}
333
+ disabled={refreshing}
334
+ label={refreshing ? "Refreshing..." : "Refresh"}
335
+ onPress={onRefresh}
336
+ />
337
+ <ActionButton theme={theme} label="Settings" onPress={onOpenSettings} />
338
+ </View>
339
+
340
+ {expanded && audit ? (
341
+ <View style={{ gap: 8 }}>
342
+ {mcpCount > 0 ? (
343
+ <Text style={mutedStyle}>
344
+ MCP coverage applies to new agents. Existing agents may need recreation or manual
345
+ setup.
346
+ </Text>
347
+ ) : null}
348
+ <View style={rowStyle}>
349
+ {audit.providers.map((provider) => (
350
+ <ProviderCoverage key={provider.provider} provider={provider} theme={theme} />
351
+ ))}
352
+ </View>
353
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
354
+ {nativeCount} native · {mcpCount} MCP · {offCount} off
355
+ </Text>
356
+ </View>
357
+ ) : null}
358
+ </Card>
359
+ );
360
+ }
361
+
362
+ interface ProviderCoverageProps {
363
+ provider: ProviderIntegration;
364
+ theme: PluginTheme;
365
+ }
366
+
367
+ function ProviderCoverage({ provider, theme }: ProviderCoverageProps) {
368
+ const unsupported = provider.platform === null && provider.activation !== "native";
369
+ const activation = unsupported ? UNSUPPORTED : ACTIVATION[provider.activation];
370
+ return (
371
+ <StatusPill
372
+ theme={theme}
373
+ label={`${providerLabel(provider.provider)} · ${activation.label}`}
374
+ tone={activation.tone}
375
+ />
376
+ );
377
+ }
package/index.client.tsx CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { PluginClientContext, PluginScreenProps } from "@getpaseo/plugin/client";
2
- import { ContextModeFooter } from "./client/context-mode-footer";
3
- import { contributeContextModeComposerPills } from "./client/context-mode-pill";
4
- import { ContextModeSurface } from "./client/context-mode-surface";
2
+ import { ContextModeFooter } from "./client/footer";
3
+ import { contributeContextModeComposerPills } from "./client/pill";
5
4
  import { ContextModeSettingsScreen } from "./client/settings-screen";
5
+ import { ContextModeSurface } from "./client/surface";
6
6
 
7
7
  export default function contribute(client: PluginClientContext) {
8
8
  function Screen(props: PluginScreenProps) {
package/index.server.ts CHANGED
@@ -1,29 +1,21 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
2
  import { createContextModeActionHandlers } from "./server/actions";
3
- import { createContextModeAnalyticsHandler } from "./server/analytics";
3
+ import { collectContextModeAnalytics } from "./server/analytics";
4
4
  import { resolveContextModeBinary } from "./server/binary";
5
5
  import { createContextModeHandlers } from "./server/handlers";
6
- import {
7
- injectContextModeEnvironment,
8
- injectContextModeOnCreate,
9
- readProviderOverrides,
10
- } from "./server/integration";
6
+ import { injectContextModeOnCreate, readProviderOverrides } from "./server/integration";
11
7
  import { createContextModeKnowledgeHandlers } from "./server/knowledge";
8
+ import { getContextModeDoctorReport } from "./shared/actions";
9
+ import { getContextModeAnalyticsDashboard } from "./shared/analytics";
10
+ import { getContextModeIntegrationAudit, getContextModeStatus } from "./shared/contracts";
12
11
  import {
13
- contextModeSettings,
14
12
  fetchAndIndexContextMode,
15
- getContextModeAnalyticsDashboard,
16
- getContextModeDoctor,
17
- getContextModeDoctorReport,
18
- getContextModeInstallAction,
19
- getContextModeIntegrationAudit,
20
- getContextModeStats,
21
- getContextModeStatus,
22
- getContextModeUpgradeAction,
23
13
  indexContextModePath,
24
14
  purgeContextModeKnowledge,
15
+ resumeContextModeSession,
25
16
  searchContextModeKnowledge,
26
- } from "./shared";
17
+ } from "./shared/knowledge";
18
+ import { contextModeSettings } from "./shared/settings";
27
19
 
28
20
  export default function contribute(server: PluginServerContext) {
29
21
  const settings = server.registerSettings(contextModeSettings);
@@ -31,17 +23,22 @@ export default function contribute(server: PluginServerContext) {
31
23
  const knowledgeHandlers = createContextModeKnowledgeHandlers(settings);
32
24
  const actionHandlers = createContextModeActionHandlers(settings);
33
25
  server.handle(getContextModeStatus, handlers.status);
34
- server.handle(getContextModeDoctor, handlers.doctor);
35
- server.handle(getContextModeStats, handlers.stats);
36
26
  server.handle(getContextModeIntegrationAudit, handlers.audit);
37
- server.handle(getContextModeAnalyticsDashboard, createContextModeAnalyticsHandler());
27
+ server.handle(getContextModeAnalyticsDashboard, async (_input, context) =>
28
+ collectContextModeAnalytics({
29
+ providers: await readProviderOverrides(context.paseo),
30
+ resolveVersion: async () => {
31
+ const status = await handlers.status({ fresh: false }, context);
32
+ return status.state === "ready" ? status.version : null;
33
+ },
34
+ }),
35
+ );
38
36
  server.handle(searchContextModeKnowledge, knowledgeHandlers.search);
39
37
  server.handle(indexContextModePath, knowledgeHandlers.indexPath);
40
38
  server.handle(fetchAndIndexContextMode, knowledgeHandlers.fetchAndIndex);
39
+ server.handle(resumeContextModeSession, knowledgeHandlers.resume);
41
40
  server.handle(purgeContextModeKnowledge, knowledgeHandlers.purge);
42
41
  server.handle(getContextModeDoctorReport, actionHandlers.doctor);
43
- server.handle(getContextModeInstallAction, actionHandlers.install);
44
- server.handle(getContextModeUpgradeAction, actionHandlers.upgrade);
45
42
  server.before("agent.create", async ({ request }, { paseo }) => {
46
43
  const current = await settings.read();
47
44
  if (current.status !== "ready") return request;
@@ -50,11 +47,5 @@ export default function contribute(server: PluginServerContext) {
50
47
  const providers = await readProviderOverrides(paseo);
51
48
  return injectContextModeOnCreate(request, current.values, binary.launch, { providers });
52
49
  });
53
- server.before("agent.session_open", async ({ request }, { paseo }) => {
54
- const current = await settings.read();
55
- if (current.status !== "ready") return request;
56
- const providers = await readProviderOverrides(paseo);
57
- return injectContextModeEnvironment(request, current.values, { providers });
58
- });
59
50
  return () => {};
60
51
  }