@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,780 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { ExternalLink } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { type ReactNode, useMemo, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
Pressable,
|
|
8
|
+
ScrollView,
|
|
9
|
+
Text,
|
|
10
|
+
TextInput,
|
|
11
|
+
type TextStyle,
|
|
12
|
+
View,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import {
|
|
16
|
+
type ContextModeProvider,
|
|
17
|
+
fetchAndIndexContextMode,
|
|
18
|
+
indexContextModePath,
|
|
19
|
+
type KnowledgeToolResult,
|
|
20
|
+
purgeContextModeKnowledge,
|
|
21
|
+
searchContextModeKnowledge,
|
|
22
|
+
} from "../shared/knowledge";
|
|
23
|
+
|
|
24
|
+
const PROVIDERS = [
|
|
25
|
+
{ value: "claude", label: "Claude" },
|
|
26
|
+
{ value: "codex", label: "Codex" },
|
|
27
|
+
{ value: "copilot", label: "Copilot" },
|
|
28
|
+
{ value: "cursor", label: "Cursor" },
|
|
29
|
+
{ value: "opencode", label: "OpenCode" },
|
|
30
|
+
{ value: "pi", label: "Pi" },
|
|
31
|
+
{ value: "omp", label: "OMP" },
|
|
32
|
+
{ value: "omp-plugin", label: "OMP plugin" },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const INSIGHT_URL = "https://context-mode.com/insight";
|
|
36
|
+
type Provider = ContextModeProvider;
|
|
37
|
+
type PurgeScope = "project" | "session";
|
|
38
|
+
export type KnowledgeSectionProps = Pick<PluginSurfaceProps, "theme" | "layout" | "host">;
|
|
39
|
+
|
|
40
|
+
interface KnowledgeStyles {
|
|
41
|
+
section: ViewStyle;
|
|
42
|
+
sectionHeader: ViewStyle;
|
|
43
|
+
sectionTitle: TextStyle;
|
|
44
|
+
card: ViewStyle;
|
|
45
|
+
cardHeader: ViewStyle;
|
|
46
|
+
cardTitle: TextStyle;
|
|
47
|
+
muted: TextStyle;
|
|
48
|
+
scopeNote: TextStyle;
|
|
49
|
+
field: ViewStyle;
|
|
50
|
+
fieldLabel: TextStyle;
|
|
51
|
+
input: TextStyle;
|
|
52
|
+
placeholder: TextStyle;
|
|
53
|
+
choiceRow: ViewStyle;
|
|
54
|
+
choice: ViewStyle;
|
|
55
|
+
choiceSelected: ViewStyle;
|
|
56
|
+
choiceText: TextStyle;
|
|
57
|
+
choiceTextSelected: TextStyle;
|
|
58
|
+
actions: ViewStyle;
|
|
59
|
+
button: ViewStyle;
|
|
60
|
+
primaryButton: ViewStyle;
|
|
61
|
+
dangerButton: ViewStyle;
|
|
62
|
+
disabled: ViewStyle;
|
|
63
|
+
buttonText: TextStyle;
|
|
64
|
+
primaryButtonText: TextStyle;
|
|
65
|
+
dangerButtonText: TextStyle;
|
|
66
|
+
linkButton: ViewStyle;
|
|
67
|
+
linkText: TextStyle;
|
|
68
|
+
pendingRow: ViewStyle;
|
|
69
|
+
activity: { color: string };
|
|
70
|
+
error: TextStyle;
|
|
71
|
+
result: ViewStyle;
|
|
72
|
+
metaRow: ViewStyle;
|
|
73
|
+
metaLabel: TextStyle;
|
|
74
|
+
metaValue: TextStyle;
|
|
75
|
+
outputFrame: ViewStyle;
|
|
76
|
+
output: TextStyle;
|
|
77
|
+
confirmation: ViewStyle;
|
|
78
|
+
confirmationTitle: TextStyle;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Field({
|
|
82
|
+
label,
|
|
83
|
+
value,
|
|
84
|
+
onChangeText,
|
|
85
|
+
placeholder,
|
|
86
|
+
styles,
|
|
87
|
+
accessibilityLabel,
|
|
88
|
+
maxLength,
|
|
89
|
+
}: {
|
|
90
|
+
label: string;
|
|
91
|
+
value: string;
|
|
92
|
+
onChangeText(value: string): void;
|
|
93
|
+
placeholder: string;
|
|
94
|
+
styles: KnowledgeStyles;
|
|
95
|
+
accessibilityLabel: string;
|
|
96
|
+
maxLength?: number;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.field}>
|
|
100
|
+
<Text style={styles.fieldLabel}>{label}</Text>
|
|
101
|
+
<TextInput
|
|
102
|
+
accessibilityLabel={accessibilityLabel}
|
|
103
|
+
autoCapitalize="none"
|
|
104
|
+
maxLength={maxLength}
|
|
105
|
+
autoCorrect={false}
|
|
106
|
+
onChangeText={onChangeText}
|
|
107
|
+
placeholder={placeholder}
|
|
108
|
+
placeholderTextColor={styles.placeholder.color}
|
|
109
|
+
style={styles.input}
|
|
110
|
+
value={value}
|
|
111
|
+
/>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ActionButton({
|
|
117
|
+
label,
|
|
118
|
+
onPress,
|
|
119
|
+
disabled = false,
|
|
120
|
+
danger = false,
|
|
121
|
+
primary = false,
|
|
122
|
+
styles,
|
|
123
|
+
}: {
|
|
124
|
+
label: string;
|
|
125
|
+
onPress(): void;
|
|
126
|
+
disabled?: boolean;
|
|
127
|
+
danger?: boolean;
|
|
128
|
+
primary?: boolean;
|
|
129
|
+
styles: KnowledgeStyles;
|
|
130
|
+
}) {
|
|
131
|
+
return (
|
|
132
|
+
<Pressable
|
|
133
|
+
accessibilityLabel={label}
|
|
134
|
+
accessibilityRole="button"
|
|
135
|
+
accessibilityState={{ disabled }}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
onPress={onPress}
|
|
138
|
+
style={[
|
|
139
|
+
styles.button,
|
|
140
|
+
primary ? styles.primaryButton : null,
|
|
141
|
+
danger ? styles.dangerButton : null,
|
|
142
|
+
disabled ? styles.disabled : null,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
<Text
|
|
146
|
+
style={[
|
|
147
|
+
styles.buttonText,
|
|
148
|
+
primary ? styles.primaryButtonText : null,
|
|
149
|
+
danger ? styles.dangerButtonText : null,
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
{label}
|
|
153
|
+
</Text>
|
|
154
|
+
</Pressable>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function WorkflowCard({
|
|
159
|
+
title,
|
|
160
|
+
description,
|
|
161
|
+
children,
|
|
162
|
+
styles,
|
|
163
|
+
}: {
|
|
164
|
+
title: string;
|
|
165
|
+
description: string;
|
|
166
|
+
children: ReactNode;
|
|
167
|
+
styles: KnowledgeStyles;
|
|
168
|
+
}) {
|
|
169
|
+
return (
|
|
170
|
+
<View style={styles.card}>
|
|
171
|
+
<View style={styles.cardHeader}>
|
|
172
|
+
<Text accessibilityRole="header" style={styles.cardTitle}>
|
|
173
|
+
{title}
|
|
174
|
+
</Text>
|
|
175
|
+
<Text style={styles.muted}>{description}</Text>
|
|
176
|
+
</View>
|
|
177
|
+
{children}
|
|
178
|
+
</View>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ResultPanel({
|
|
183
|
+
result,
|
|
184
|
+
pending,
|
|
185
|
+
error,
|
|
186
|
+
emptyLabel,
|
|
187
|
+
source,
|
|
188
|
+
styles,
|
|
189
|
+
}: {
|
|
190
|
+
result: KnowledgeToolResult | undefined;
|
|
191
|
+
pending: boolean;
|
|
192
|
+
error: unknown;
|
|
193
|
+
emptyLabel: string;
|
|
194
|
+
source?: string;
|
|
195
|
+
styles: KnowledgeStyles;
|
|
196
|
+
}) {
|
|
197
|
+
if (pending) {
|
|
198
|
+
return (
|
|
199
|
+
<View accessibilityLiveRegion="polite" style={styles.pendingRow}>
|
|
200
|
+
<ActivityIndicator size="small" color={styles.activity.color} />
|
|
201
|
+
<Text style={styles.muted}>Working…</Text>
|
|
202
|
+
</View>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (error) {
|
|
206
|
+
return (
|
|
207
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
208
|
+
{error instanceof Error ? error.message : String(error)}
|
|
209
|
+
</Text>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (!result) return null;
|
|
213
|
+
return (
|
|
214
|
+
<View accessibilityLiveRegion="polite" style={styles.result}>
|
|
215
|
+
<View style={styles.metaRow}>
|
|
216
|
+
<Text style={styles.metaLabel}>Provider</Text>
|
|
217
|
+
<Text selectable style={styles.metaValue}>
|
|
218
|
+
{result.provider}
|
|
219
|
+
</Text>
|
|
220
|
+
</View>
|
|
221
|
+
{source ? (
|
|
222
|
+
<View style={styles.metaRow}>
|
|
223
|
+
<Text style={styles.metaLabel}>Source</Text>
|
|
224
|
+
<Text selectable style={styles.metaValue}>
|
|
225
|
+
{source}
|
|
226
|
+
</Text>
|
|
227
|
+
</View>
|
|
228
|
+
) : null}
|
|
229
|
+
<ScrollView nestedScrollEnabled style={styles.outputFrame}>
|
|
230
|
+
<Text selectable style={styles.output}>
|
|
231
|
+
{result.output || emptyLabel}
|
|
232
|
+
</Text>
|
|
233
|
+
</ScrollView>
|
|
234
|
+
</View>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function KnowledgeSection({ theme, layout, host }: KnowledgeSectionProps) {
|
|
239
|
+
const styles = useMemo(() => createStyles(theme, layout.compact), [layout.compact, theme]);
|
|
240
|
+
const searchKnowledge = useRpc(searchContextModeKnowledge);
|
|
241
|
+
const indexPath = useRpc(indexContextModePath);
|
|
242
|
+
const fetchAndIndex = useRpc(fetchAndIndexContextMode);
|
|
243
|
+
const purgeKnowledge = useRpc(purgeContextModeKnowledge);
|
|
244
|
+
const queryClient = useQueryClient();
|
|
245
|
+
const refreshAnalytics = () =>
|
|
246
|
+
queryClient.invalidateQueries({
|
|
247
|
+
queryKey: ["context-mode", host.id, "analytics-dashboard"],
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const [provider, setProvider] = useState<Provider | null>("claude");
|
|
251
|
+
const [projectPath, setProjectPath] = useState("");
|
|
252
|
+
const [query, setQuery] = useState("");
|
|
253
|
+
const [searchSource, setSearchSource] = useState("");
|
|
254
|
+
const [path, setPath] = useState("");
|
|
255
|
+
const [pathSource, setPathSource] = useState("");
|
|
256
|
+
const [url, setUrl] = useState("");
|
|
257
|
+
const [urlSource, setUrlSource] = useState("");
|
|
258
|
+
const [purgeScope, setPurgeScope] = useState<PurgeScope | null>(null);
|
|
259
|
+
const [searchResultSource, setSearchResultSource] = useState("");
|
|
260
|
+
const [pathResultSource, setPathResultSource] = useState("");
|
|
261
|
+
const [urlResultSource, setUrlResultSource] = useState("");
|
|
262
|
+
const [sessionId, setSessionId] = useState("");
|
|
263
|
+
const [managementExpanded, setManagementExpanded] = useState(false);
|
|
264
|
+
const [purgeArmed, setPurgeArmed] = useState(false);
|
|
265
|
+
const [linkError, setLinkError] = useState<string | null>(null);
|
|
266
|
+
|
|
267
|
+
const search = useMutation({
|
|
268
|
+
mutationFn: () => {
|
|
269
|
+
if (!provider) throw new Error("Select a provider store before searching.");
|
|
270
|
+
const source = searchSource.trim();
|
|
271
|
+
setSearchResultSource(source);
|
|
272
|
+
return searchKnowledge({
|
|
273
|
+
provider,
|
|
274
|
+
projectPath: projectPath.trim(),
|
|
275
|
+
queries: [query.trim()],
|
|
276
|
+
...(source ? { source } : {}),
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const index = useMutation({
|
|
281
|
+
mutationFn: () => {
|
|
282
|
+
if (!provider) throw new Error("Select a provider store before indexing.");
|
|
283
|
+
const source = pathSource.trim();
|
|
284
|
+
setPathResultSource(source || path.trim());
|
|
285
|
+
return indexPath({
|
|
286
|
+
provider,
|
|
287
|
+
projectPath: projectPath.trim(),
|
|
288
|
+
path: path.trim(),
|
|
289
|
+
...(source ? { source } : {}),
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
onSuccess: refreshAnalytics,
|
|
293
|
+
});
|
|
294
|
+
const fetch = useMutation({
|
|
295
|
+
mutationFn: () => {
|
|
296
|
+
if (!provider) throw new Error("Select a provider store before fetching.");
|
|
297
|
+
const source = urlSource.trim();
|
|
298
|
+
setUrlResultSource(source || url.trim());
|
|
299
|
+
return fetchAndIndex({
|
|
300
|
+
provider,
|
|
301
|
+
projectPath: projectPath.trim(),
|
|
302
|
+
url: url.trim(),
|
|
303
|
+
...(source ? { source } : {}),
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
onSuccess: refreshAnalytics,
|
|
307
|
+
});
|
|
308
|
+
const purge = useMutation({
|
|
309
|
+
mutationFn: () => {
|
|
310
|
+
if (!provider || !purgeScope) throw new Error("Select a provider and purge scope.");
|
|
311
|
+
if (purgeScope === "session") {
|
|
312
|
+
return purgeKnowledge({
|
|
313
|
+
provider,
|
|
314
|
+
projectPath: projectPath.trim(),
|
|
315
|
+
confirm: true,
|
|
316
|
+
scope: "session",
|
|
317
|
+
sessionId: sessionId.trim(),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return purgeKnowledge({
|
|
321
|
+
provider,
|
|
322
|
+
projectPath: projectPath.trim(),
|
|
323
|
+
confirm: true,
|
|
324
|
+
scope: "project",
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
onSuccess: refreshAnalytics,
|
|
328
|
+
onSettled: () => setPurgeArmed(false),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const busy = search.isPending || index.isPending || fetch.isPending || purge.isPending;
|
|
332
|
+
const changeProvider = (next: Provider) => {
|
|
333
|
+
setProvider(next);
|
|
334
|
+
setPurgeArmed(false);
|
|
335
|
+
search.reset();
|
|
336
|
+
index.reset();
|
|
337
|
+
fetch.reset();
|
|
338
|
+
purge.reset();
|
|
339
|
+
};
|
|
340
|
+
const changePurgeScope = (next: PurgeScope) => {
|
|
341
|
+
setPurgeScope(next);
|
|
342
|
+
setPurgeArmed(false);
|
|
343
|
+
purge.reset();
|
|
344
|
+
};
|
|
345
|
+
const providerReady = provider !== null && projectPath.trim().length > 0;
|
|
346
|
+
const canPreparePurge =
|
|
347
|
+
providerReady &&
|
|
348
|
+
purgeScope !== null &&
|
|
349
|
+
(purgeScope !== "session" || sessionId.trim().length > 0);
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<View style={styles.section}>
|
|
353
|
+
<View style={styles.sectionHeader}>
|
|
354
|
+
<Text accessibilityRole="header" style={styles.sectionTitle}>
|
|
355
|
+
Knowledge
|
|
356
|
+
</Text>
|
|
357
|
+
<Text style={styles.muted}>
|
|
358
|
+
Search or add material in one provider-specific store. Nothing is copied between
|
|
359
|
+
providers.
|
|
360
|
+
</Text>
|
|
361
|
+
</View>
|
|
362
|
+
|
|
363
|
+
<View style={styles.card}>
|
|
364
|
+
<Text style={styles.fieldLabel}>Provider store</Text>
|
|
365
|
+
<View accessibilityRole="radiogroup" style={styles.choiceRow}>
|
|
366
|
+
{PROVIDERS.map((option) => {
|
|
367
|
+
const selected = provider === option.value;
|
|
368
|
+
return (
|
|
369
|
+
<Pressable
|
|
370
|
+
key={option.value}
|
|
371
|
+
accessibilityLabel={`${option.label} provider store`}
|
|
372
|
+
disabled={busy}
|
|
373
|
+
accessibilityRole="radio"
|
|
374
|
+
accessibilityState={{ checked: selected, disabled: busy }}
|
|
375
|
+
onPress={() => changeProvider(option.value)}
|
|
376
|
+
style={[
|
|
377
|
+
styles.choice,
|
|
378
|
+
selected ? styles.choiceSelected : null,
|
|
379
|
+
busy ? styles.disabled : null,
|
|
380
|
+
]}
|
|
381
|
+
>
|
|
382
|
+
<Text style={[styles.choiceText, selected ? styles.choiceTextSelected : null]}>
|
|
383
|
+
{option.label}
|
|
384
|
+
</Text>
|
|
385
|
+
</Pressable>
|
|
386
|
+
);
|
|
387
|
+
})}
|
|
388
|
+
</View>
|
|
389
|
+
<Field
|
|
390
|
+
accessibilityLabel="Knowledge project path"
|
|
391
|
+
label="Project path"
|
|
392
|
+
maxLength={4_096}
|
|
393
|
+
onChangeText={(value) => {
|
|
394
|
+
setProjectPath(value);
|
|
395
|
+
setPurgeArmed(false);
|
|
396
|
+
}}
|
|
397
|
+
placeholder="/absolute/path/to/project"
|
|
398
|
+
styles={styles}
|
|
399
|
+
value={projectPath}
|
|
400
|
+
/>
|
|
401
|
+
<Text accessibilityLiveRegion="polite" style={styles.scopeNote}>
|
|
402
|
+
{provider && projectPath.trim()
|
|
403
|
+
? `Current destination: ${PROVIDERS.find((item) => item.value === provider)?.label} storage for ${projectPath.trim()}.`
|
|
404
|
+
: "Select a provider and absolute project path. No storage destination is selected by default."}
|
|
405
|
+
</Text>
|
|
406
|
+
</View>
|
|
407
|
+
|
|
408
|
+
<WorkflowCard
|
|
409
|
+
title="Search"
|
|
410
|
+
description="Search indexed knowledge. Add a source filter when you only want matches from one named source."
|
|
411
|
+
styles={styles}
|
|
412
|
+
>
|
|
413
|
+
<Field
|
|
414
|
+
accessibilityLabel="Knowledge search query"
|
|
415
|
+
label="Query"
|
|
416
|
+
maxLength={1_024}
|
|
417
|
+
onChangeText={setQuery}
|
|
418
|
+
placeholder="What do you need to find?"
|
|
419
|
+
styles={styles}
|
|
420
|
+
value={query}
|
|
421
|
+
/>
|
|
422
|
+
<Field
|
|
423
|
+
accessibilityLabel="Knowledge search source filter"
|
|
424
|
+
label="Source filter (optional)"
|
|
425
|
+
maxLength={512}
|
|
426
|
+
onChangeText={setSearchSource}
|
|
427
|
+
placeholder="docs, repository, or source label"
|
|
428
|
+
styles={styles}
|
|
429
|
+
value={searchSource}
|
|
430
|
+
/>
|
|
431
|
+
<View style={styles.actions}>
|
|
432
|
+
<ActionButton
|
|
433
|
+
disabled={!providerReady || query.trim().length === 0 || search.isPending}
|
|
434
|
+
label={search.isPending ? "Searching…" : "Search knowledge"}
|
|
435
|
+
onPress={() => search.mutate()}
|
|
436
|
+
primary
|
|
437
|
+
styles={styles}
|
|
438
|
+
/>
|
|
439
|
+
</View>
|
|
440
|
+
<ResultPanel
|
|
441
|
+
emptyLabel="No matching knowledge was returned."
|
|
442
|
+
error={search.error}
|
|
443
|
+
pending={search.isPending}
|
|
444
|
+
result={search.data}
|
|
445
|
+
source={searchResultSource || undefined}
|
|
446
|
+
styles={styles}
|
|
447
|
+
/>
|
|
448
|
+
</WorkflowCard>
|
|
449
|
+
|
|
450
|
+
<Pressable
|
|
451
|
+
accessibilityLabel={managementExpanded ? "Hide source management" : "Add or manage sources"}
|
|
452
|
+
accessibilityRole="button"
|
|
453
|
+
accessibilityState={{ expanded: managementExpanded }}
|
|
454
|
+
onPress={() => setManagementExpanded((expanded) => !expanded)}
|
|
455
|
+
style={styles.button}
|
|
456
|
+
>
|
|
457
|
+
<Text style={styles.buttonText}>
|
|
458
|
+
{managementExpanded ? "Hide source management" : "Add or manage sources"}
|
|
459
|
+
</Text>
|
|
460
|
+
</Pressable>
|
|
461
|
+
|
|
462
|
+
{managementExpanded ? (
|
|
463
|
+
<>
|
|
464
|
+
<WorkflowCard
|
|
465
|
+
title="Source visibility"
|
|
466
|
+
description="Open Context Mode Insight to inspect indexed sources and understand what is available to search."
|
|
467
|
+
styles={styles}
|
|
468
|
+
>
|
|
469
|
+
<View style={styles.actions}>
|
|
470
|
+
<ExternalLink
|
|
471
|
+
accessibilityLabel="Open Context Mode Insight in a browser"
|
|
472
|
+
href={INSIGHT_URL}
|
|
473
|
+
onError={(error) =>
|
|
474
|
+
setLinkError(error instanceof Error ? error.message : String(error))
|
|
475
|
+
}
|
|
476
|
+
>
|
|
477
|
+
<View style={styles.linkButton}>
|
|
478
|
+
<Text style={styles.linkText}>Open Insight</Text>
|
|
479
|
+
</View>
|
|
480
|
+
</ExternalLink>
|
|
481
|
+
</View>
|
|
482
|
+
{linkError ? (
|
|
483
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
484
|
+
Could not open Insight: {linkError}
|
|
485
|
+
</Text>
|
|
486
|
+
) : null}
|
|
487
|
+
</WorkflowCard>
|
|
488
|
+
|
|
489
|
+
<WorkflowCard
|
|
490
|
+
title="Index a local path"
|
|
491
|
+
description="Add a file or directory from the selected provider environment. The source label stays visible with the indexed material."
|
|
492
|
+
styles={styles}
|
|
493
|
+
>
|
|
494
|
+
<Field
|
|
495
|
+
accessibilityLabel="Local path to index"
|
|
496
|
+
label="Local path"
|
|
497
|
+
maxLength={4_096}
|
|
498
|
+
onChangeText={setPath}
|
|
499
|
+
placeholder="/absolute/path/to/docs"
|
|
500
|
+
styles={styles}
|
|
501
|
+
value={path}
|
|
502
|
+
/>
|
|
503
|
+
<Field
|
|
504
|
+
accessibilityLabel="Local path source label"
|
|
505
|
+
label="Source label (optional)"
|
|
506
|
+
maxLength={512}
|
|
507
|
+
onChangeText={setPathSource}
|
|
508
|
+
placeholder="project-docs"
|
|
509
|
+
styles={styles}
|
|
510
|
+
value={pathSource}
|
|
511
|
+
/>
|
|
512
|
+
<View style={styles.actions}>
|
|
513
|
+
<ActionButton
|
|
514
|
+
disabled={!providerReady || path.trim().length === 0 || index.isPending}
|
|
515
|
+
label={index.isPending ? "Indexing…" : "Index path"}
|
|
516
|
+
onPress={() => index.mutate()}
|
|
517
|
+
styles={styles}
|
|
518
|
+
/>
|
|
519
|
+
</View>
|
|
520
|
+
<ResultPanel
|
|
521
|
+
emptyLabel="Context Mode completed without a text report."
|
|
522
|
+
error={index.error}
|
|
523
|
+
pending={index.isPending}
|
|
524
|
+
result={index.data}
|
|
525
|
+
source={pathResultSource || undefined}
|
|
526
|
+
styles={styles}
|
|
527
|
+
/>
|
|
528
|
+
</WorkflowCard>
|
|
529
|
+
|
|
530
|
+
<WorkflowCard
|
|
531
|
+
title="Fetch and index a URL"
|
|
532
|
+
description="Fetch one HTTP or HTTPS source and add it to the selected provider store."
|
|
533
|
+
styles={styles}
|
|
534
|
+
>
|
|
535
|
+
<Field
|
|
536
|
+
accessibilityLabel="URL to fetch and index"
|
|
537
|
+
label="URL"
|
|
538
|
+
maxLength={4_096}
|
|
539
|
+
onChangeText={setUrl}
|
|
540
|
+
placeholder="https://docs.example.com/guide"
|
|
541
|
+
styles={styles}
|
|
542
|
+
value={url}
|
|
543
|
+
/>
|
|
544
|
+
<Field
|
|
545
|
+
accessibilityLabel="Fetched URL source label"
|
|
546
|
+
label="Source label (optional)"
|
|
547
|
+
maxLength={512}
|
|
548
|
+
onChangeText={setUrlSource}
|
|
549
|
+
placeholder="product-guide"
|
|
550
|
+
styles={styles}
|
|
551
|
+
value={urlSource}
|
|
552
|
+
/>
|
|
553
|
+
<View style={styles.actions}>
|
|
554
|
+
<ActionButton
|
|
555
|
+
disabled={!providerReady || url.trim().length === 0 || fetch.isPending}
|
|
556
|
+
label={fetch.isPending ? "Fetching…" : "Fetch and index"}
|
|
557
|
+
onPress={() => fetch.mutate()}
|
|
558
|
+
styles={styles}
|
|
559
|
+
/>
|
|
560
|
+
</View>
|
|
561
|
+
<ResultPanel
|
|
562
|
+
emptyLabel="Context Mode completed without a text report."
|
|
563
|
+
error={fetch.error}
|
|
564
|
+
pending={fetch.isPending}
|
|
565
|
+
result={fetch.data}
|
|
566
|
+
source={urlResultSource || undefined}
|
|
567
|
+
styles={styles}
|
|
568
|
+
/>
|
|
569
|
+
</WorkflowCard>
|
|
570
|
+
|
|
571
|
+
<WorkflowCard
|
|
572
|
+
title="Purge knowledge"
|
|
573
|
+
description="Purge only the selected provider store and scope. Select a scope, review it, then confirm."
|
|
574
|
+
styles={styles}
|
|
575
|
+
>
|
|
576
|
+
<Text style={styles.fieldLabel}>Scope</Text>
|
|
577
|
+
<View accessibilityRole="radiogroup" style={styles.choiceRow}>
|
|
578
|
+
{(["project", "session"] as const).map((scope) => {
|
|
579
|
+
const selected = purgeScope === scope;
|
|
580
|
+
const label = scope === "project" ? "Project" : "Session";
|
|
581
|
+
return (
|
|
582
|
+
<Pressable
|
|
583
|
+
key={scope}
|
|
584
|
+
accessibilityLabel={`Purge ${label.toLowerCase()} scope`}
|
|
585
|
+
accessibilityRole="radio"
|
|
586
|
+
accessibilityState={{ checked: selected }}
|
|
587
|
+
onPress={() => changePurgeScope(scope)}
|
|
588
|
+
style={[styles.choice, selected ? styles.choiceSelected : null]}
|
|
589
|
+
>
|
|
590
|
+
<Text style={[styles.choiceText, selected ? styles.choiceTextSelected : null]}>
|
|
591
|
+
{label}
|
|
592
|
+
</Text>
|
|
593
|
+
</Pressable>
|
|
594
|
+
);
|
|
595
|
+
})}
|
|
596
|
+
</View>
|
|
597
|
+
{purgeScope === "session" ? (
|
|
598
|
+
<Field
|
|
599
|
+
accessibilityLabel="Session identifier to purge"
|
|
600
|
+
label="Session ID"
|
|
601
|
+
maxLength={256}
|
|
602
|
+
onChangeText={(value) => {
|
|
603
|
+
setSessionId(value);
|
|
604
|
+
setPurgeArmed(false);
|
|
605
|
+
}}
|
|
606
|
+
placeholder="Exact Context Mode session ID"
|
|
607
|
+
styles={styles}
|
|
608
|
+
value={sessionId}
|
|
609
|
+
/>
|
|
610
|
+
) : null}
|
|
611
|
+
{!purgeArmed ? (
|
|
612
|
+
<View style={styles.actions}>
|
|
613
|
+
<ActionButton
|
|
614
|
+
danger
|
|
615
|
+
disabled={!canPreparePurge || purge.isPending}
|
|
616
|
+
label="Review purge"
|
|
617
|
+
onPress={() => setPurgeArmed(true)}
|
|
618
|
+
styles={styles}
|
|
619
|
+
/>
|
|
620
|
+
</View>
|
|
621
|
+
) : (
|
|
622
|
+
<View accessibilityRole="alert" style={styles.confirmation}>
|
|
623
|
+
<Text style={styles.confirmationTitle}>Confirm permanent purge</Text>
|
|
624
|
+
<Text style={styles.muted}>
|
|
625
|
+
{`Delete ${purgeScope === "session" ? `session ${sessionId.trim()}` : "project"} knowledge from ${provider} storage? This does not affect other providers.`}
|
|
626
|
+
</Text>
|
|
627
|
+
<View style={styles.actions}>
|
|
628
|
+
<ActionButton
|
|
629
|
+
danger
|
|
630
|
+
label={purge.isPending ? "Purging…" : "Confirm purge"}
|
|
631
|
+
onPress={() => purge.mutate()}
|
|
632
|
+
styles={styles}
|
|
633
|
+
/>
|
|
634
|
+
<ActionButton
|
|
635
|
+
disabled={purge.isPending}
|
|
636
|
+
label="Cancel"
|
|
637
|
+
onPress={() => setPurgeArmed(false)}
|
|
638
|
+
styles={styles}
|
|
639
|
+
/>
|
|
640
|
+
</View>
|
|
641
|
+
</View>
|
|
642
|
+
)}
|
|
643
|
+
<ResultPanel
|
|
644
|
+
emptyLabel="The selected knowledge scope was purged."
|
|
645
|
+
error={purge.error}
|
|
646
|
+
pending={purge.isPending}
|
|
647
|
+
result={purge.data}
|
|
648
|
+
styles={styles}
|
|
649
|
+
/>
|
|
650
|
+
</WorkflowCard>
|
|
651
|
+
</>
|
|
652
|
+
) : null}
|
|
653
|
+
</View>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function createStyles(theme: PluginSurfaceProps["theme"], compact: boolean): KnowledgeStyles {
|
|
658
|
+
return {
|
|
659
|
+
section: { width: "100%" as const, gap: compact ? 12 : 16 },
|
|
660
|
+
sectionHeader: { gap: 4 },
|
|
661
|
+
sectionTitle: {
|
|
662
|
+
color: theme.colors.foreground,
|
|
663
|
+
fontSize: compact ? 18 : 20,
|
|
664
|
+
fontWeight: "700" as const,
|
|
665
|
+
},
|
|
666
|
+
card: {
|
|
667
|
+
width: "100%" as const,
|
|
668
|
+
gap: 12,
|
|
669
|
+
padding: compact ? 12 : 16,
|
|
670
|
+
borderWidth: 1,
|
|
671
|
+
borderColor: theme.colors.border,
|
|
672
|
+
borderRadius: 12,
|
|
673
|
+
backgroundColor: theme.colors.surface1,
|
|
674
|
+
},
|
|
675
|
+
cardHeader: { gap: 3 },
|
|
676
|
+
cardTitle: { color: theme.colors.foreground, fontSize: 15, fontWeight: "700" as const },
|
|
677
|
+
muted: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
678
|
+
scopeNote: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
679
|
+
field: { gap: 5 },
|
|
680
|
+
fieldLabel: {
|
|
681
|
+
color: theme.colors.foregroundMuted,
|
|
682
|
+
fontSize: 12,
|
|
683
|
+
fontWeight: "600" as const,
|
|
684
|
+
},
|
|
685
|
+
input: {
|
|
686
|
+
minHeight: 40,
|
|
687
|
+
width: "100%" as const,
|
|
688
|
+
borderWidth: 1,
|
|
689
|
+
borderColor: theme.colors.border,
|
|
690
|
+
borderRadius: 8,
|
|
691
|
+
paddingHorizontal: 11,
|
|
692
|
+
paddingVertical: 8,
|
|
693
|
+
color: theme.colors.foreground,
|
|
694
|
+
backgroundColor: theme.colors.surface0,
|
|
695
|
+
fontSize: 14,
|
|
696
|
+
},
|
|
697
|
+
placeholder: { color: theme.colors.foregroundMuted },
|
|
698
|
+
choiceRow: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 7 },
|
|
699
|
+
choice: {
|
|
700
|
+
minHeight: 36,
|
|
701
|
+
justifyContent: "center" as const,
|
|
702
|
+
paddingHorizontal: 11,
|
|
703
|
+
paddingVertical: 7,
|
|
704
|
+
borderWidth: 1,
|
|
705
|
+
borderColor: theme.colors.border,
|
|
706
|
+
borderRadius: 999,
|
|
707
|
+
backgroundColor: theme.colors.surface0,
|
|
708
|
+
},
|
|
709
|
+
choiceSelected: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
|
|
710
|
+
choiceText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" as const },
|
|
711
|
+
choiceTextSelected: { color: theme.colors.accentForeground },
|
|
712
|
+
actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 8 },
|
|
713
|
+
button: {
|
|
714
|
+
minHeight: 38,
|
|
715
|
+
alignItems: "center" as const,
|
|
716
|
+
justifyContent: "center" as const,
|
|
717
|
+
paddingHorizontal: 13,
|
|
718
|
+
paddingVertical: 8,
|
|
719
|
+
borderWidth: 1,
|
|
720
|
+
borderColor: theme.colors.border,
|
|
721
|
+
borderRadius: 8,
|
|
722
|
+
backgroundColor: theme.colors.surface2,
|
|
723
|
+
},
|
|
724
|
+
primaryButton: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
|
|
725
|
+
dangerButton: {
|
|
726
|
+
borderColor: theme.colors.statusDanger,
|
|
727
|
+
backgroundColor: theme.colors.surface1,
|
|
728
|
+
},
|
|
729
|
+
disabled: { opacity: 0.5 },
|
|
730
|
+
buttonText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" as const },
|
|
731
|
+
primaryButtonText: { color: theme.colors.accentForeground },
|
|
732
|
+
dangerButtonText: { color: theme.colors.statusDanger },
|
|
733
|
+
linkButton: {
|
|
734
|
+
minHeight: 38,
|
|
735
|
+
alignItems: "center" as const,
|
|
736
|
+
justifyContent: "center" as const,
|
|
737
|
+
paddingHorizontal: 13,
|
|
738
|
+
paddingVertical: 8,
|
|
739
|
+
borderWidth: 1,
|
|
740
|
+
borderColor: theme.colors.border,
|
|
741
|
+
borderRadius: 8,
|
|
742
|
+
backgroundColor: theme.colors.surface2,
|
|
743
|
+
},
|
|
744
|
+
linkText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" as const },
|
|
745
|
+
pendingRow: { flexDirection: "row" as const, alignItems: "center" as const, gap: 8 },
|
|
746
|
+
activity: { color: theme.colors.accent },
|
|
747
|
+
error: { color: theme.colors.statusDanger, fontSize: 12, lineHeight: 17 },
|
|
748
|
+
result: {
|
|
749
|
+
gap: 7,
|
|
750
|
+
padding: 10,
|
|
751
|
+
borderWidth: 1,
|
|
752
|
+
borderColor: theme.colors.border,
|
|
753
|
+
borderRadius: 8,
|
|
754
|
+
backgroundColor: theme.colors.surface0,
|
|
755
|
+
},
|
|
756
|
+
metaRow: { flexDirection: "row" as const, alignItems: "flex-start" as const, gap: 8 },
|
|
757
|
+
metaLabel: { width: 58, color: theme.colors.foregroundMuted, fontSize: 11 },
|
|
758
|
+
metaValue: { flex: 1, color: theme.colors.foreground, fontSize: 11 },
|
|
759
|
+
outputFrame: { maxHeight: compact ? 160 : 220 },
|
|
760
|
+
output: {
|
|
761
|
+
color: theme.colors.foreground,
|
|
762
|
+
fontFamily: "monospace",
|
|
763
|
+
fontSize: compact ? 11 : 12,
|
|
764
|
+
lineHeight: compact ? 16 : 18,
|
|
765
|
+
},
|
|
766
|
+
confirmation: {
|
|
767
|
+
gap: 9,
|
|
768
|
+
padding: 11,
|
|
769
|
+
borderWidth: 1,
|
|
770
|
+
borderColor: theme.colors.statusDanger,
|
|
771
|
+
borderRadius: 8,
|
|
772
|
+
backgroundColor: theme.colors.surface0,
|
|
773
|
+
},
|
|
774
|
+
confirmationTitle: {
|
|
775
|
+
color: theme.colors.statusDanger,
|
|
776
|
+
fontSize: 13,
|
|
777
|
+
fontWeight: "700" as const,
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
}
|