@omercnet/paseo-context-mode 1.1.0 → 1.2.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/README.md CHANGED
@@ -32,7 +32,9 @@ These previews were captured from an isolated Paseo test daemon with Context Mod
32
32
  - Provides provider- and project-scoped knowledge search, local-path indexing, URL fetch/index, indexed-source visibility, and an Insight link.
33
33
  - Requires an explicit provider, project path, scope review, and second confirmation before purge. Purge remains delegated to Context Mode's own MCP contract.
34
34
  - Returns structured install and provider-specific upgrade argv for review; Paseo never executes those commands automatically.
35
- - Adds a per-agent composer shortcut to the Context Mode surface.
35
+ - On Paseo 0.11, registers the `context-mode` screen with URL-persisted `section`, `agentId`, and `workspaceId` params. Composer pills open Knowledge using the selected host's agent provider and working directory. URL params never carry paths or queries.
36
+ - Custom or named providers that do not exactly match a Knowledge RPC provider require explicit provider selection. Missing agents or mismatched workspaces also require explicit scope selection.
37
+ - Adds a live Healthy/Failing sidebar footer row on Paseo 0.11, reusing the screen's health data. Paseo 0.9/0.10 retain the static sidebar, Savings surface, and unscoped composer shortcut.
36
38
 
37
39
  Context Mode currently couples sessions, stats, and indexed content under `CONTEXT_MODE_DIR`. Until upstream supports separate session and content roots, this plugin keeps storage provider-scoped instead of risking cross-provider session contamination.
38
40
 
@@ -0,0 +1,54 @@
1
+ import { type PluginSidebarItemProps, useRpc, useSettings } from "@getpaseo/plugin/client";
2
+ import { SidebarRow } from "@getpaseo/plugin/client/ui";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { Text } from "react-native";
5
+ import { contextModeSettings, getContextModeStatus } from "../shared";
6
+
7
+ // The footer stays mounted all session; poll slowly because each status miss spawns the binary.
8
+ const FOOTER_REFRESH_MS = 60_000;
9
+
10
+ export function ContextModeFooter({
11
+ theme,
12
+ host,
13
+ currentScreen,
14
+ openScreen,
15
+ }: PluginSidebarItemProps) {
16
+ const settings = useSettings(contextModeSettings);
17
+ const loadStatus = useRpc(getContextModeStatus);
18
+ const status = useQuery({
19
+ queryKey: ["context-mode", host.id, "status"],
20
+ queryFn: () => loadStatus({ fresh: false }),
21
+ refetchInterval: FOOTER_REFRESH_MS,
22
+ staleTime: FOOTER_REFRESH_MS,
23
+ enabled: settings.status === "ready",
24
+ });
25
+ const failing =
26
+ settings.status === "error" ||
27
+ settings.status === "invalid" ||
28
+ Boolean(status.error) ||
29
+ (status.data && status.data.state !== "ready");
30
+ const healthy = settings.status === "ready" && !failing && status.data?.state === "ready";
31
+ const label = failing ? "Failing" : healthy ? "Healthy" : "Checking";
32
+ return (
33
+ <SidebarRow
34
+ icon="Gauge"
35
+ label="Context Mode"
36
+ active={currentScreen?.screenId === "context-mode"}
37
+ onPress={() => openScreen({ screenId: "context-mode" })}
38
+ trailing={
39
+ <Text
40
+ style={{
41
+ color: failing
42
+ ? theme.colors.statusDanger
43
+ : healthy
44
+ ? theme.colors.statusSuccess
45
+ : theme.colors.foregroundMuted,
46
+ fontSize: 12,
47
+ }}
48
+ >
49
+ {label}
50
+ </Text>
51
+ }
52
+ />
53
+ );
54
+ }
@@ -50,7 +50,14 @@ export function contributeContextModeComposerPills(client: PluginClientContext):
50
50
  behavior: {
51
51
  kind: "action",
52
52
  onPress() {
53
- client.openSurface("context-mode");
53
+ if (typeof client.addScreen === "function" && typeof client.openScreen === "function") {
54
+ client.openScreen({
55
+ screenId: "context-mode",
56
+ params: { section: "knowledge", agentId: agent.id, workspaceId: agent.workspaceId },
57
+ });
58
+ } else {
59
+ client.openSurface("context-mode");
60
+ }
54
61
  },
55
62
  },
56
63
  },
@@ -1,7 +1,7 @@
1
1
  import { type PluginSurfaceProps, useRpc, useSettings } from "@getpaseo/plugin/client";
2
2
  import { Icon } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
- import { useMemo, useState } from "react";
4
+ import { useEffect, useMemo, useState } from "react";
5
5
  import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
6
6
  import {
7
7
  type BinaryStatus,
@@ -14,9 +14,12 @@ import {
14
14
  import { ActionsSection } from "./actions-section";
15
15
  import { AnalyticsDashboard } from "./analytics-dashboard";
16
16
  import { KnowledgeSection } from "./knowledge-section";
17
+ import { ScopedKnowledgeSection } from "./scoped-knowledge-section";
18
+ import { parseScreenParams, type SurfaceTab } from "./screen-scope";
17
19
 
18
20
  interface ContextModeSurfaceProps extends PluginSurfaceProps {
19
21
  onOpenSettings(): void;
22
+ params?: Record<string, string>;
20
23
  }
21
24
 
22
25
  function statusLabel(status: BinaryStatus): string {
@@ -26,8 +29,6 @@ function statusLabel(status: BinaryStatus): string {
26
29
  return "Unavailable";
27
30
  }
28
31
 
29
- type SurfaceTab = "savings" | "knowledge" | "setup";
30
-
31
32
  const SURFACE_TABS: ReadonlyArray<{ id: SurfaceTab; label: string }> = [
32
33
  { id: "savings", label: "Savings" },
33
34
  { id: "knowledge", label: "Knowledge" },
@@ -39,13 +40,19 @@ export function ContextModeSurface({
39
40
  host,
40
41
  layout,
41
42
  onOpenSettings,
43
+ params,
42
44
  }: ContextModeSurfaceProps) {
43
45
  const settings = useSettings(contextModeSettings);
44
46
  const loadStatus = useRpc(getContextModeStatus);
45
47
  const loadStats = useRpc(getContextModeStats);
46
48
  const loadIntegrationAudit = useRpc(getContextModeIntegrationAudit);
47
49
  const queryClient = useQueryClient();
48
- const [activeTab, setActiveTab] = useState<SurfaceTab>("savings");
50
+ const screenParams = parseScreenParams(params ?? {});
51
+ const [activeTab, setActiveTab] = useState<SurfaceTab>(screenParams.section);
52
+ useEffect(
53
+ () => setActiveTab(screenParams.section),
54
+ [screenParams.section, screenParams.agentId, screenParams.workspaceId],
55
+ );
49
56
  const readySettings = settings.status === "ready" ? settings : null;
50
57
  const refreshInterval = readySettings?.values.refreshIntervalMs ?? 15_000;
51
58
  const statusKey = ["context-mode", host.id, "status"] as const;
@@ -273,7 +280,17 @@ export function ContextModeSurface({
273
280
  />
274
281
  </View>
275
282
  <View style={[styles.tabPanel, activeTab === "knowledge" ? null : styles.tabPanelHidden]}>
276
- <KnowledgeSection key={host.id} host={host} layout={layout} theme={theme} />
283
+ {activeTab !== "knowledge" ? null : screenParams.agentId || screenParams.workspaceId ? (
284
+ <ScopedKnowledgeSection
285
+ key={JSON.stringify([host.id, screenParams.agentId, screenParams.workspaceId])}
286
+ params={screenParams}
287
+ host={host}
288
+ layout={layout}
289
+ theme={theme}
290
+ />
291
+ ) : (
292
+ <KnowledgeSection key={host.id} host={host} layout={layout} theme={theme} />
293
+ )}
277
294
  </View>
278
295
  <View style={[styles.tabPanel, activeTab === "setup" ? null : styles.tabPanelHidden]}>
279
296
  <ActionsSection key={host.id} layout={layout} theme={theme} />
@@ -20,6 +20,7 @@ import {
20
20
  purgeContextModeKnowledge,
21
21
  searchContextModeKnowledge,
22
22
  } from "../shared/knowledge";
23
+ import type { KnowledgeScope } from "./screen-scope";
23
24
 
24
25
  const PROVIDERS = [
25
26
  { value: "claude", label: "Claude" },
@@ -35,7 +36,9 @@ const PROVIDERS = [
35
36
  const INSIGHT_URL = "https://context-mode.com/insight";
36
37
  type Provider = ContextModeProvider;
37
38
  type PurgeScope = "project" | "session";
38
- export type KnowledgeSectionProps = Pick<PluginSurfaceProps, "theme" | "layout" | "host">;
39
+ export type KnowledgeSectionProps = Pick<PluginSurfaceProps, "theme" | "layout" | "host"> & {
40
+ initialScope?: KnowledgeScope;
41
+ };
39
42
 
40
43
  interface KnowledgeStyles {
41
44
  section: ViewStyle;
@@ -235,7 +238,7 @@ function ResultPanel({
235
238
  );
236
239
  }
237
240
 
238
- export function KnowledgeSection({ theme, layout, host }: KnowledgeSectionProps) {
241
+ export function KnowledgeSection({ theme, layout, host, initialScope }: KnowledgeSectionProps) {
239
242
  const styles = useMemo(() => createStyles(theme, layout.compact), [layout.compact, theme]);
240
243
  const searchKnowledge = useRpc(searchContextModeKnowledge);
241
244
  const indexPath = useRpc(indexContextModePath);
@@ -247,8 +250,10 @@ export function KnowledgeSection({ theme, layout, host }: KnowledgeSectionProps)
247
250
  queryKey: ["context-mode", host.id, "analytics-dashboard"],
248
251
  });
249
252
 
250
- const [provider, setProvider] = useState<Provider | null>("claude");
251
- const [projectPath, setProjectPath] = useState("");
253
+ const [provider, setProvider] = useState<Provider | null>(
254
+ initialScope ? initialScope.provider : "claude",
255
+ );
256
+ const [projectPath, setProjectPath] = useState(initialScope?.projectPath ?? "");
252
257
  const [query, setQuery] = useState("");
253
258
  const [searchSource, setSearchSource] = useState("");
254
259
  const [path, setPath] = useState("");
@@ -362,6 +367,7 @@ export function KnowledgeSection({ theme, layout, host }: KnowledgeSectionProps)
362
367
 
363
368
  <View style={styles.card}>
364
369
  <Text style={styles.fieldLabel}>Provider store</Text>
370
+ {initialScope?.note ? <Text style={styles.scopeNote}>{initialScope.note}</Text> : null}
365
371
  <View accessibilityRole="radiogroup" style={styles.choiceRow}>
366
372
  {PROVIDERS.map((option) => {
367
373
  const selected = provider === option.value;
@@ -0,0 +1,48 @@
1
+ import { type PluginSurfaceProps, usePaseo } from "@getpaseo/plugin/client";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { Text, View } from "react-native";
4
+ import { KnowledgeSection } from "./knowledge-section";
5
+ import { resolveKnowledgeScope, type ScreenParams } from "./screen-scope";
6
+
7
+ export function ScopedKnowledgeSection({
8
+ params,
9
+ ...props
10
+ }: PluginSurfaceProps & { params: ScreenParams }) {
11
+ const paseo = usePaseo();
12
+ const scope = useQuery({
13
+ queryKey: [
14
+ "context-mode",
15
+ props.host.id,
16
+ "knowledge-scope",
17
+ params.agentId,
18
+ params.workspaceId,
19
+ ],
20
+ queryFn: () => resolveKnowledgeScope(paseo, params),
21
+ retry: false,
22
+ });
23
+ if (scope.isPending)
24
+ return (
25
+ <Text style={{ color: props.theme.colors.foregroundMuted }}>Resolving Knowledge scope…</Text>
26
+ );
27
+ if (scope.error)
28
+ return (
29
+ <View>
30
+ <Text accessibilityRole="alert" style={{ color: props.theme.colors.statusDanger }}>
31
+ {scope.error.message}
32
+ </Text>
33
+ <KnowledgeSection
34
+ key="scope-error"
35
+ {...props}
36
+ initialScope={{ provider: null, projectPath: "" }}
37
+ />
38
+ </View>
39
+ );
40
+ // Remount only when the resolved destination changes so stale form state never survives it.
41
+ return (
42
+ <KnowledgeSection
43
+ key={JSON.stringify([scope.data.provider, scope.data.projectPath])}
44
+ {...props}
45
+ initialScope={scope.data}
46
+ />
47
+ );
48
+ }
@@ -0,0 +1,61 @@
1
+ import type { PaseoApi } from "@getpaseo/client";
2
+ import { type ContextModeProvider, ContextModeProviderSchema } from "../shared/knowledge";
3
+
4
+ export type SurfaceTab = "savings" | "knowledge" | "setup";
5
+ export type KnowledgeScope = {
6
+ provider: ContextModeProvider | null;
7
+ projectPath: string;
8
+ note?: string;
9
+ };
10
+
11
+ export type ScreenParams = { section: SurfaceTab; agentId?: string; workspaceId?: string };
12
+ export function parseScreenParams(params: Record<string, string>): ScreenParams {
13
+ const section: SurfaceTab =
14
+ params.section === "knowledge" || params.section === "setup" ? params.section : "savings";
15
+ return {
16
+ section,
17
+ agentId: params.agentId || undefined,
18
+ workspaceId: params.workspaceId || undefined,
19
+ };
20
+ }
21
+
22
+ export function knowledgeProvider(provider: string): ContextModeProvider | null {
23
+ const parsed = ContextModeProviderSchema.safeParse(provider);
24
+ return parsed.success ? parsed.data : null;
25
+ }
26
+
27
+ export async function resolveKnowledgeScope(
28
+ paseo: PaseoApi,
29
+ params: ScreenParams,
30
+ ): Promise<KnowledgeScope> {
31
+ if (params.agentId) {
32
+ const result = await paseo.agents.ref(params.agentId).refresh();
33
+ if (!result)
34
+ throw new Error("Agent not found on this host. Select a provider and project explicitly.");
35
+ if (params.workspaceId && result.agent.workspaceId !== params.workspaceId) {
36
+ throw new Error(
37
+ "Agent no longer belongs to this workspace. Select a provider and project explicitly.",
38
+ );
39
+ }
40
+ const provider = knowledgeProvider(result.agent.provider);
41
+ return {
42
+ provider,
43
+ projectPath: result.agent.cwd,
44
+ ...(!provider
45
+ ? {
46
+ note: `Provider ${result.agent.provider} cannot be mapped to a knowledge store. Select one explicitly.`,
47
+ }
48
+ : {}),
49
+ };
50
+ }
51
+ const workspace = params.workspaceId
52
+ ? await paseo.workspaces.ref(params.workspaceId).refresh()
53
+ : null;
54
+ if (!workspace)
55
+ throw new Error("Workspace not found on this host. Select a provider and project explicitly.");
56
+ return {
57
+ provider: null,
58
+ projectPath: workspace.workspaceDirectory,
59
+ note: "Select a provider store for this workspace.",
60
+ };
61
+ }
package/index.client.tsx CHANGED
@@ -1,4 +1,10 @@
1
- import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
1
+ import type {
2
+ PluginClientContext,
3
+ PluginScreenProps,
4
+ PluginSurfaceProps,
5
+ } from "@getpaseo/plugin/client";
6
+ import { SidebarRow } from "@getpaseo/plugin/client/ui";
7
+ import { ContextModeFooter } from "./client/context-mode-footer";
2
8
  import { contributeContextModeComposerPills } from "./client/context-mode-pill";
3
9
  import { ContextModeSurface } from "./client/context-mode-surface";
4
10
  import { ContextModeSettingsScreen } from "./client/settings-screen";
@@ -9,6 +15,12 @@ export default function contribute(client: PluginClientContext) {
9
15
  <ContextModeSurface {...props} onOpenSettings={() => client.openSettings("context-mode")} />
10
16
  );
11
17
  }
18
+ function Screen(props: PluginScreenProps) {
19
+ return (
20
+ <ContextModeSurface {...props} onOpenSettings={() => client.openSettings("context-mode")} />
21
+ );
22
+ }
23
+ const screens = typeof client.addScreen === "function" && typeof client.openScreen === "function";
12
24
 
13
25
  const cleanups = [
14
26
  contributeContextModeComposerPills(client),
@@ -18,21 +30,44 @@ export default function contribute(client: PluginClientContext) {
18
30
  icon: "Settings",
19
31
  Component: ContextModeSettingsScreen,
20
32
  }),
21
- client.addSurface("context-mode", Surface),
22
- client.addSidebarItem({
23
- id: "context-mode",
24
- title: "Context Mode",
25
- icon: "Gauge",
26
- surface: "context-mode",
27
- }),
33
+ ...(screens
34
+ ? [
35
+ client.addScreen({ id: "context-mode", title: "Context Mode", Component: Screen }),
36
+ ...(typeof client.addSidebarFooterItem === "function" && typeof SidebarRow === "function"
37
+ ? [
38
+ client.addSidebarFooterItem({
39
+ id: "context-mode",
40
+ title: "Context Mode",
41
+ Component: ContextModeFooter,
42
+ }),
43
+ ]
44
+ : [
45
+ client.addSidebarItem({
46
+ id: "context-mode",
47
+ title: "Context Mode",
48
+ icon: "Gauge",
49
+ surface: "context-mode",
50
+ }),
51
+ ]),
52
+ ]
53
+ : [
54
+ client.addSurface("context-mode", Surface),
55
+ client.addSidebarItem({
56
+ id: "context-mode",
57
+ title: "Context Mode",
58
+ icon: "Gauge",
59
+ surface: "context-mode",
60
+ }),
61
+ ]),
28
62
  client.addCommandCenterItem({
29
63
  id: "open-context-mode",
30
64
  title: "Open Context Mode",
31
65
  icon: "Gauge",
32
66
  keywords: ["context", "savings", "tokens", "knowledge", "search", "doctor", "health"],
33
67
  context: "global",
34
- onSelect({ openSurface }) {
35
- openSurface("context-mode");
68
+ onSelect({ openSurface, openScreen }) {
69
+ if (screens && typeof openScreen === "function") openScreen({ screenId: "context-mode" });
70
+ else openSurface("context-mode");
36
71
  },
37
72
  }),
38
73
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-context-mode",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Provider-aware Context Mode health, savings, and MCP activation for Paseo.",
6
6
  "license": "MIT",
@@ -50,10 +50,10 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@biomejs/biome": "^2.5.10",
53
- "@getpaseo/cli": "0.10.1",
54
- "@getpaseo/client": "0.10.1",
55
- "@getpaseo/plugin": "0.10.1",
56
- "@getpaseo/protocol": "0.10.1",
53
+ "@getpaseo/cli": "0.11.0-beta.3",
54
+ "@getpaseo/client": "0.11.0-beta.3",
55
+ "@getpaseo/plugin": "0.11.0-beta.3",
56
+ "@getpaseo/protocol": "0.11.0-beta.3",
57
57
  "@tanstack/react-query": "^5.102.3",
58
58
  "@types/node": "^24.10.1",
59
59
  "@types/react": "^19.2.18",
package/paseo-plugin.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "id": "context-mode",
3
3
  "description": "Provider-aware Context Mode health, savings, and MCP activation for Paseo.",
4
- "requirements": { "paseo": "^0.9.0 || ^0.10.0" }
4
+ "requirements": { "paseo": "^0.9.0 || ^0.10.0 || ^0.11.0" }
5
5
  }