@omercnet/paseo-context-mode 3.1.4-next.382.1 → 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.
- package/OVERVIEW.md +3 -3
- package/README.md +13 -15
- package/client/analytics-dashboard.tsx +745 -613
- package/client/controls.tsx +295 -0
- package/client/diagnostics-section.tsx +178 -0
- package/client/{context-mode-footer.tsx → footer.tsx} +22 -20
- package/client/knowledge-section.tsx +725 -697
- package/client/{context-mode-pill.tsx → pill.tsx} +5 -5
- package/client/scoped-knowledge-section.tsx +14 -20
- package/client/screen-scope.ts +12 -6
- package/client/settings-screen.tsx +94 -97
- package/client/surface.tsx +377 -0
- package/index.client.tsx +3 -3
- package/index.server.ts +18 -27
- package/npm-shrinkwrap.json +6 -1745
- package/package.json +3 -3
- package/server/actions.ts +91 -102
- package/server/analytics.ts +257 -308
- package/server/binary.ts +119 -94
- package/server/database.ts +463 -0
- package/server/handlers.ts +40 -95
- package/server/integration.ts +230 -361
- package/server/knowledge.ts +65 -137
- package/server/mcp-process.ts +135 -86
- package/server/storage-discovery.ts +46 -199
- package/shared/actions.ts +2 -27
- package/shared/analytics.ts +60 -33
- package/shared/contracts.ts +16 -47
- package/shared/knowledge.ts +62 -65
- package/shared/providers.ts +62 -0
- package/shared/settings.ts +33 -18
- package/client/actions-section.tsx +0 -621
- package/client/context-mode-surface.tsx +0 -444
- package/server/context-mode-db.ts +0 -481
- package/shared/index.ts +0 -5
|
@@ -1,230 +1,261 @@
|
|
|
1
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
1
2
|
import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
-
import {
|
|
3
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { type ReactNode,
|
|
5
|
-
import {
|
|
6
|
-
ActivityIndicator,
|
|
7
|
-
Pressable,
|
|
8
|
-
ScrollView,
|
|
9
|
-
Text,
|
|
10
|
-
TextInput,
|
|
11
|
-
type TextStyle,
|
|
12
|
-
View,
|
|
13
|
-
type ViewStyle,
|
|
14
|
-
} from "react-native";
|
|
3
|
+
import { ScrollView, SettingsSection, TextInput } from "@getpaseo/plugin/client/ui";
|
|
4
|
+
import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { type ReactNode, useState } from "react";
|
|
6
|
+
import { Text, View } from "react-native";
|
|
15
7
|
import {
|
|
8
|
+
type FetchAndIndexInput,
|
|
16
9
|
fetchAndIndexContextMode,
|
|
10
|
+
type IndexPathInput,
|
|
17
11
|
indexContextModePath,
|
|
12
|
+
type KnowledgeDestination,
|
|
18
13
|
type KnowledgeToolResult,
|
|
14
|
+
type PurgeKnowledgeInput,
|
|
19
15
|
purgeContextModeKnowledge,
|
|
16
|
+
type SearchKnowledgeInput,
|
|
17
|
+
type SearchKnowledgeResult,
|
|
18
|
+
type SearchPage,
|
|
19
|
+
sameDestination,
|
|
20
20
|
searchContextModeKnowledge,
|
|
21
21
|
} from "../shared/knowledge";
|
|
22
|
+
import { CONTEXT_MODE_PROVIDERS, IN_PROCESS_PROVIDERS } from "../shared/providers";
|
|
23
|
+
import { ActionButton, Card, type ChoiceOption, Choices, ErrorText, Pending } from "./controls";
|
|
22
24
|
import type { KnowledgeScope } from "./screen-scope";
|
|
23
25
|
|
|
24
|
-
const PROVIDER_LABELS: Readonly<Record<string, string>> = {
|
|
25
|
-
claude: "Claude",
|
|
26
|
-
codex: "Codex",
|
|
27
|
-
copilot: "Copilot",
|
|
28
|
-
cursor: "Cursor",
|
|
29
|
-
opencode: "OpenCode",
|
|
30
|
-
pi: "Pi",
|
|
31
|
-
omp: "OMP",
|
|
32
|
-
"omp-plugin": "OMP plugin",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
26
|
/** Used until the integration audit reports the configured providers. */
|
|
36
|
-
export const BUILTIN_KNOWLEDGE_PROVIDERS =
|
|
27
|
+
export const BUILTIN_KNOWLEDGE_PROVIDERS: readonly string[] = CONTEXT_MODE_PROVIDERS.map(
|
|
28
|
+
({ provider }) => provider,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/** Built-in providers by name; configured providers (for example `claude-work`) by their id. */
|
|
32
|
+
export function providerLabel(id: string): string {
|
|
33
|
+
const entry = [...CONTEXT_MODE_PROVIDERS, ...IN_PROCESS_PROVIDERS].find(
|
|
34
|
+
({ provider }) => provider === id,
|
|
35
|
+
);
|
|
36
|
+
return entry ? entry.label : id;
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
type SearchScope = SearchKnowledgeInput["scope"];
|
|
40
|
+
type SearchSort = SearchKnowledgeInput["sort"];
|
|
41
|
+
type PurgeScope = PurgeKnowledgeInput["scope"];
|
|
39
42
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const SEARCH_SCOPES: readonly ChoiceOption<SearchScope>[] = [
|
|
44
|
+
{ value: "worktree", label: "Worktree" },
|
|
45
|
+
{ value: "project", label: "Project" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Timeline is upstream's oldest-first order. */
|
|
49
|
+
const SEARCH_SORTS: readonly ChoiceOption<SearchSort>[] = [
|
|
50
|
+
{ value: "relevance", label: "Relevance" },
|
|
51
|
+
{ value: "timeline", label: "Timeline" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const PURGE_SCOPES: readonly ChoiceOption<PurgeScope>[] = [
|
|
55
|
+
{ value: "project", label: "Project" },
|
|
56
|
+
{ value: "session", label: "Session" },
|
|
57
|
+
{ value: "source", label: "Source" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** What `ctx_purge` removes per scope: a session purge deletes only its journal, never indexed sources. */
|
|
61
|
+
const PURGE_CONSEQUENCE: Readonly<Record<PurgeScope, string>> = {
|
|
62
|
+
project: "Every agent working in this project loses its recorded activity and indexed sources.",
|
|
63
|
+
session: "The session's recorded activity and resume state are deleted; indexed sources stay.",
|
|
64
|
+
source: "Every agent working in this project loses it.",
|
|
47
65
|
};
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
type PurgeTarget =
|
|
68
|
+
| { scope: "project" }
|
|
69
|
+
| { scope: "session"; sessionId: string }
|
|
70
|
+
| { scope: "source"; source: string };
|
|
71
|
+
type ForgetTarget = Exclude<PurgeTarget, { scope: "project" }>;
|
|
72
|
+
|
|
73
|
+
interface MoreResultsInput {
|
|
74
|
+
request: SearchKnowledgeInput;
|
|
75
|
+
page: SearchPage;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function knowledgeMutationKey(hostId: string) {
|
|
79
|
+
return ["context-mode", hostId, "knowledge"] as const;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function useRefreshAnalytics(hostId: string): () => Promise<void> {
|
|
83
|
+
const queryClient = useQueryClient();
|
|
84
|
+
return () =>
|
|
85
|
+
queryClient.invalidateQueries({ queryKey: ["context-mode", hostId, "analytics-dashboard"] });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function describePurgeTarget(target: PurgeTarget): string {
|
|
89
|
+
if (target.scope === "session") return `session ${target.sessionId}`;
|
|
90
|
+
if (target.scope === "source") return `source ${target.source}`;
|
|
91
|
+
return "all project knowledge";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Drops agent-only footers (the More results button pages instead) and tool-name prefixes. */
|
|
95
|
+
export function presentKnowledgeOutput(output: string): string {
|
|
96
|
+
return output
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.filter((line) => !/^[Mm]ore: ctx_search\(/.test(line))
|
|
99
|
+
.map((line) => line.replace(/^ctx_[a-z_]+: /, ""))
|
|
100
|
+
.join("\n")
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createStyles(theme: PluginTheme) {
|
|
105
|
+
return {
|
|
106
|
+
muted: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 16 },
|
|
107
|
+
fieldLabel: { color: theme.colors.foregroundMuted, fontSize: 12, fontWeight: "500" as const },
|
|
108
|
+
cardTitle: { color: theme.colors.foreground, fontSize: 14, fontWeight: "500" as const },
|
|
109
|
+
actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 8 },
|
|
110
|
+
result: {
|
|
111
|
+
gap: 8,
|
|
112
|
+
padding: 12,
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderColor: theme.colors.border,
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
backgroundColor: theme.colors.surface0,
|
|
117
|
+
},
|
|
118
|
+
metaRow: { flexDirection: "row" as const, alignItems: "flex-start" as const, gap: 8 },
|
|
119
|
+
metaLabel: { width: 56, color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
120
|
+
metaValue: { flex: 1, color: theme.colors.foreground, fontSize: 12 },
|
|
121
|
+
output: {
|
|
122
|
+
color: theme.colors.foreground,
|
|
123
|
+
fontFamily: "monospace",
|
|
124
|
+
fontSize: 12,
|
|
125
|
+
lineHeight: 16,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface FieldProps {
|
|
131
|
+
theme: PluginTheme;
|
|
132
|
+
label: string;
|
|
133
|
+
value: string;
|
|
134
|
+
onChangeText(value: string): void;
|
|
135
|
+
placeholder: string;
|
|
136
|
+
accessibilityLabel: string;
|
|
137
|
+
maxLength: number;
|
|
138
|
+
disabled?: boolean;
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
function Field({
|
|
142
|
+
theme,
|
|
91
143
|
label,
|
|
92
144
|
value,
|
|
93
145
|
onChangeText,
|
|
94
146
|
placeholder,
|
|
95
|
-
styles,
|
|
96
147
|
accessibilityLabel,
|
|
97
148
|
maxLength,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
onChangeText(value: string): void;
|
|
102
|
-
placeholder: string;
|
|
103
|
-
styles: KnowledgeStyles;
|
|
104
|
-
accessibilityLabel: string;
|
|
105
|
-
maxLength?: number;
|
|
106
|
-
}) {
|
|
149
|
+
disabled = false,
|
|
150
|
+
}: FieldProps) {
|
|
151
|
+
const styles = createStyles(theme);
|
|
107
152
|
return (
|
|
108
|
-
<View style={
|
|
153
|
+
<View style={{ gap: 4 }}>
|
|
109
154
|
<Text style={styles.fieldLabel}>{label}</Text>
|
|
110
155
|
<TextInput
|
|
111
156
|
accessibilityLabel={accessibilityLabel}
|
|
112
157
|
autoCapitalize="none"
|
|
113
|
-
maxLength={maxLength}
|
|
114
158
|
autoCorrect={false}
|
|
159
|
+
editable={!disabled}
|
|
160
|
+
maxLength={maxLength}
|
|
115
161
|
onChangeText={onChangeText}
|
|
116
162
|
placeholder={placeholder}
|
|
117
|
-
placeholderTextColor={
|
|
118
|
-
style={
|
|
163
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
164
|
+
style={{
|
|
165
|
+
minHeight: 32,
|
|
166
|
+
width: "100%",
|
|
167
|
+
borderWidth: 1,
|
|
168
|
+
borderColor: theme.colors.border,
|
|
169
|
+
borderRadius: 8,
|
|
170
|
+
paddingHorizontal: 12,
|
|
171
|
+
paddingVertical: 8,
|
|
172
|
+
color: theme.colors.foreground,
|
|
173
|
+
backgroundColor: theme.colors.surface0,
|
|
174
|
+
fontSize: 14,
|
|
175
|
+
}}
|
|
119
176
|
value={value}
|
|
120
177
|
/>
|
|
121
178
|
</View>
|
|
122
179
|
);
|
|
123
180
|
}
|
|
124
181
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
onPress,
|
|
128
|
-
disabled = false,
|
|
129
|
-
danger = false,
|
|
130
|
-
primary = false,
|
|
131
|
-
styles,
|
|
132
|
-
}: {
|
|
133
|
-
label: string;
|
|
134
|
-
onPress(): void;
|
|
135
|
-
disabled?: boolean;
|
|
136
|
-
danger?: boolean;
|
|
137
|
-
primary?: boolean;
|
|
138
|
-
styles: KnowledgeStyles;
|
|
139
|
-
}) {
|
|
140
|
-
return (
|
|
141
|
-
<Pressable
|
|
142
|
-
accessibilityLabel={label}
|
|
143
|
-
accessibilityRole="button"
|
|
144
|
-
accessibilityState={{ disabled }}
|
|
145
|
-
disabled={disabled}
|
|
146
|
-
onPress={onPress}
|
|
147
|
-
style={[
|
|
148
|
-
styles.button,
|
|
149
|
-
primary ? styles.primaryButton : null,
|
|
150
|
-
danger ? styles.dangerButton : null,
|
|
151
|
-
disabled ? styles.disabled : null,
|
|
152
|
-
]}
|
|
153
|
-
>
|
|
154
|
-
<Text
|
|
155
|
-
style={[
|
|
156
|
-
styles.buttonText,
|
|
157
|
-
primary ? styles.primaryButtonText : null,
|
|
158
|
-
danger ? styles.dangerButtonText : null,
|
|
159
|
-
]}
|
|
160
|
-
>
|
|
161
|
-
{label}
|
|
162
|
-
</Text>
|
|
163
|
-
</Pressable>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function WorkflowCard({
|
|
168
|
-
title,
|
|
169
|
-
description,
|
|
170
|
-
children,
|
|
171
|
-
styles,
|
|
172
|
-
}: {
|
|
182
|
+
interface WorkflowCardProps {
|
|
183
|
+
theme: PluginTheme;
|
|
173
184
|
title: string;
|
|
174
185
|
description: string;
|
|
175
186
|
children: ReactNode;
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function WorkflowCard({ theme, title, description, children }: WorkflowCardProps) {
|
|
190
|
+
const styles = createStyles(theme);
|
|
178
191
|
return (
|
|
179
|
-
<
|
|
180
|
-
<View style={
|
|
192
|
+
<Card theme={theme}>
|
|
193
|
+
<View style={{ gap: 4 }}>
|
|
181
194
|
<Text accessibilityRole="header" style={styles.cardTitle}>
|
|
182
195
|
{title}
|
|
183
196
|
</Text>
|
|
184
197
|
<Text style={styles.muted}>{description}</Text>
|
|
185
198
|
</View>
|
|
186
199
|
{children}
|
|
200
|
+
</Card>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface LabeledChoicesProps<Value extends string> {
|
|
205
|
+
theme: PluginTheme;
|
|
206
|
+
label: string;
|
|
207
|
+
options: readonly ChoiceOption<Value>[];
|
|
208
|
+
value: Value | null;
|
|
209
|
+
onChange(value: Value): void;
|
|
210
|
+
accessibilityLabel(option: ChoiceOption<Value>): string;
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function LabeledChoices<Value extends string>({ label, ...choices }: LabeledChoicesProps<Value>) {
|
|
215
|
+
return (
|
|
216
|
+
<View style={{ gap: 4 }}>
|
|
217
|
+
<Text style={createStyles(choices.theme).fieldLabel}>{label}</Text>
|
|
218
|
+
<Choices {...choices} />
|
|
187
219
|
</View>
|
|
188
220
|
);
|
|
189
221
|
}
|
|
190
222
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
interface OutputFrameProps {
|
|
224
|
+
theme: PluginTheme;
|
|
225
|
+
output: string;
|
|
226
|
+
emptyLabel: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function OutputFrame({ theme, output, emptyLabel }: OutputFrameProps) {
|
|
230
|
+
return (
|
|
231
|
+
<ScrollView nestedScrollEnabled style={{ maxHeight: 220 }}>
|
|
232
|
+
<Text selectable style={createStyles(theme).output}>
|
|
233
|
+
{presentKnowledgeOutput(output) || emptyLabel}
|
|
234
|
+
</Text>
|
|
235
|
+
</ScrollView>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface ResultPanelProps {
|
|
240
|
+
theme: PluginTheme;
|
|
199
241
|
result: KnowledgeToolResult | undefined;
|
|
200
242
|
pending: boolean;
|
|
201
243
|
error: unknown;
|
|
202
244
|
emptyLabel: string;
|
|
203
245
|
source?: string;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<Text style={styles.muted}>Working…</Text>
|
|
211
|
-
</View>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
if (error) {
|
|
215
|
-
return (
|
|
216
|
-
<Text accessibilityRole="alert" style={styles.error}>
|
|
217
|
-
{error instanceof Error ? error.message : String(error)}
|
|
218
|
-
</Text>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function ResultPanel({ theme, result, pending, error, emptyLabel, source }: ResultPanelProps) {
|
|
249
|
+
const styles = createStyles(theme);
|
|
250
|
+
if (pending) return <Pending theme={theme} label="Working..." />;
|
|
251
|
+
if (error) return <ErrorText theme={theme} error={error} />;
|
|
221
252
|
if (!result) return null;
|
|
222
253
|
return (
|
|
223
254
|
<View accessibilityLiveRegion="polite" style={styles.result}>
|
|
224
255
|
<View style={styles.metaRow}>
|
|
225
|
-
<Text style={styles.metaLabel}>
|
|
256
|
+
<Text style={styles.metaLabel}>Agent</Text>
|
|
226
257
|
<Text selectable style={styles.metaValue}>
|
|
227
|
-
{result.provider}
|
|
258
|
+
{providerLabel(result.provider)}
|
|
228
259
|
</Text>
|
|
229
260
|
</View>
|
|
230
261
|
{source ? (
|
|
@@ -235,571 +266,568 @@ function ResultPanel({
|
|
|
235
266
|
</Text>
|
|
236
267
|
</View>
|
|
237
268
|
) : null}
|
|
238
|
-
<
|
|
239
|
-
<Text selectable style={styles.output}>
|
|
240
|
-
{result.output || emptyLabel}
|
|
241
|
-
</Text>
|
|
242
|
-
</ScrollView>
|
|
269
|
+
<OutputFrame theme={theme} output={result.output} emptyLabel={emptyLabel} />
|
|
243
270
|
</View>
|
|
244
271
|
);
|
|
245
272
|
}
|
|
246
273
|
|
|
247
|
-
|
|
274
|
+
interface ConfirmPanelProps {
|
|
275
|
+
theme: PluginTheme;
|
|
276
|
+
message: string;
|
|
277
|
+
confirmLabel: string;
|
|
278
|
+
pendingLabel: string;
|
|
279
|
+
pending: boolean;
|
|
280
|
+
confirmAccessibilityLabel?: string;
|
|
281
|
+
onConfirm(): void;
|
|
282
|
+
onCancel(): void;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The review step before a permanent delete: the only place a destructive button appears. */
|
|
286
|
+
function ConfirmPanel({
|
|
248
287
|
theme,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
288
|
+
message,
|
|
289
|
+
confirmLabel,
|
|
290
|
+
pendingLabel,
|
|
291
|
+
pending,
|
|
292
|
+
confirmAccessibilityLabel,
|
|
293
|
+
onConfirm,
|
|
294
|
+
onCancel,
|
|
295
|
+
}: ConfirmPanelProps) {
|
|
296
|
+
const styles = createStyles(theme);
|
|
297
|
+
return (
|
|
298
|
+
<View
|
|
299
|
+
accessibilityRole="alert"
|
|
300
|
+
style={{
|
|
301
|
+
gap: 8,
|
|
302
|
+
padding: 12,
|
|
303
|
+
borderWidth: 1,
|
|
304
|
+
borderColor: theme.colors.statusDanger,
|
|
305
|
+
borderRadius: 8,
|
|
306
|
+
backgroundColor: theme.colors.surface0,
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
<Text style={styles.muted}>{message}</Text>
|
|
310
|
+
<View style={styles.actions}>
|
|
311
|
+
<ActionButton
|
|
312
|
+
theme={theme}
|
|
313
|
+
variant="destructive"
|
|
314
|
+
accessibilityLabel={confirmAccessibilityLabel}
|
|
315
|
+
disabled={pending}
|
|
316
|
+
label={pending ? pendingLabel : confirmLabel}
|
|
317
|
+
onPress={onConfirm}
|
|
318
|
+
/>
|
|
319
|
+
<ActionButton theme={theme} disabled={pending} label="Cancel" onPress={onCancel} />
|
|
320
|
+
</View>
|
|
321
|
+
</View>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
264
324
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
325
|
+
export interface KnowledgeSectionProps extends Pick<PluginSurfaceProps, "theme" | "host"> {
|
|
326
|
+
initialScope?: KnowledgeScope;
|
|
327
|
+
/** Paseo provider ids Context Mode supports: the built-ins plus configured providers extending one. */
|
|
328
|
+
providers: readonly string[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function initialProvider(scope: KnowledgeScope | undefined, providers: readonly string[]) {
|
|
332
|
+
if (!scope) return providers.includes("claude") ? "claude" : null;
|
|
333
|
+
if (scope.provider !== null && providers.includes(scope.provider)) return scope.provider;
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function scopeNote(scope: KnowledgeScope | undefined, providers: readonly string[]) {
|
|
338
|
+
if (scope?.note) return scope.note;
|
|
339
|
+
if (!scope?.provider || providers.includes(scope.provider)) return null;
|
|
340
|
+
return `Knowledge needs an agent Paseo can run Context Mode for, and ${scope.provider} is not one. Select another agent.`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function KnowledgeSection({ theme, host, initialScope, providers }: KnowledgeSectionProps) {
|
|
344
|
+
const styles = createStyles(theme);
|
|
345
|
+
const note = scopeNote(initialScope, providers);
|
|
346
|
+
const [provider, setProvider] = useState<string | null>(() =>
|
|
347
|
+
initialProvider(initialScope, providers),
|
|
274
348
|
);
|
|
275
349
|
const [projectPath, setProjectPath] = useState(initialScope?.projectPath ?? "");
|
|
276
|
-
const [query, setQuery] = useState("");
|
|
277
|
-
const [searchSource, setSearchSource] = useState("");
|
|
278
|
-
const [path, setPath] = useState("");
|
|
279
|
-
const [pathSource, setPathSource] = useState("");
|
|
280
|
-
const [url, setUrl] = useState("");
|
|
281
|
-
const [urlSource, setUrlSource] = useState("");
|
|
282
|
-
const [purgeScope, setPurgeScope] = useState<PurgeScope | null>(null);
|
|
283
|
-
const [searchResultSource, setSearchResultSource] = useState("");
|
|
284
|
-
const [pathResultSource, setPathResultSource] = useState("");
|
|
285
|
-
const [urlResultSource, setUrlResultSource] = useState("");
|
|
286
|
-
const [sessionId, setSessionId] = useState("");
|
|
287
350
|
const [managementExpanded, setManagementExpanded] = useState(false);
|
|
288
|
-
const
|
|
289
|
-
const
|
|
351
|
+
const busy = useIsMutating({ mutationKey: knowledgeMutationKey(host.id) }) > 0;
|
|
352
|
+
const trimmedPath = projectPath.trim();
|
|
353
|
+
const destination: KnowledgeDestination | null =
|
|
354
|
+
provider !== null && trimmedPath ? { provider, projectPath: trimmedPath } : null;
|
|
355
|
+
const destinationNote = destination
|
|
356
|
+
? `Current destination: the project store for ${destination.projectPath}, recorded as ${providerLabel(destination.provider)}.`
|
|
357
|
+
: "Select an agent and absolute project path.";
|
|
358
|
+
const providerOptions = providers.map((id) => ({ value: id, label: providerLabel(id) }));
|
|
359
|
+
// Results belong to the agent they were recorded as, so a new agent starts the workflows fresh.
|
|
360
|
+
const workflowKey = provider ?? "none";
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<SettingsSection
|
|
364
|
+
title="Knowledge"
|
|
365
|
+
info="Search or add material in the Context Mode store of one project. Every agent working in that project shares it and the agent labels who added what. A provider with its own CONTEXT_MODE_DATA_DIR uses the store in that directory."
|
|
366
|
+
>
|
|
367
|
+
<View style={{ gap: 16 }}>
|
|
368
|
+
<Card theme={theme}>
|
|
369
|
+
{note ? <Text style={styles.muted}>{note}</Text> : null}
|
|
370
|
+
<LabeledChoices
|
|
371
|
+
theme={theme}
|
|
372
|
+
label="Agent"
|
|
373
|
+
disabled={busy}
|
|
374
|
+
accessibilityLabel={(option) => `${option.label} agent`}
|
|
375
|
+
onChange={setProvider}
|
|
376
|
+
options={providerOptions}
|
|
377
|
+
value={provider}
|
|
378
|
+
/>
|
|
379
|
+
<Field
|
|
380
|
+
theme={theme}
|
|
381
|
+
accessibilityLabel="Knowledge project path"
|
|
382
|
+
label="Project path"
|
|
383
|
+
disabled={busy}
|
|
384
|
+
maxLength={4_096}
|
|
385
|
+
onChangeText={setProjectPath}
|
|
386
|
+
placeholder="/absolute/path/to/project"
|
|
387
|
+
value={projectPath}
|
|
388
|
+
/>
|
|
389
|
+
<Text accessibilityLiveRegion="polite" style={styles.muted}>
|
|
390
|
+
{destinationNote}
|
|
391
|
+
</Text>
|
|
392
|
+
</Card>
|
|
393
|
+
|
|
394
|
+
<SearchCard
|
|
395
|
+
key={`search:${workflowKey}`}
|
|
396
|
+
theme={theme}
|
|
397
|
+
hostId={host.id}
|
|
398
|
+
destination={destination}
|
|
399
|
+
/>
|
|
400
|
+
|
|
401
|
+
<ActionButton
|
|
402
|
+
theme={theme}
|
|
403
|
+
variant="outline"
|
|
404
|
+
disabled={busy}
|
|
405
|
+
expanded={managementExpanded}
|
|
406
|
+
label={managementExpanded ? "Hide source management" : "Add or manage sources"}
|
|
407
|
+
onPress={() => setManagementExpanded((expanded) => !expanded)}
|
|
408
|
+
/>
|
|
409
|
+
|
|
410
|
+
{managementExpanded ? (
|
|
411
|
+
<SourceManagement
|
|
412
|
+
key={`sources:${workflowKey}`}
|
|
413
|
+
theme={theme}
|
|
414
|
+
hostId={host.id}
|
|
415
|
+
destination={destination}
|
|
416
|
+
/>
|
|
417
|
+
) : null}
|
|
418
|
+
</View>
|
|
419
|
+
</SettingsSection>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
interface WorkflowProps {
|
|
424
|
+
theme: PluginTheme;
|
|
425
|
+
hostId: string;
|
|
426
|
+
destination: KnowledgeDestination | null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Result panel state for a mutation, shown only while its destination is still selected. */
|
|
430
|
+
function forDestination(
|
|
431
|
+
mutation: {
|
|
432
|
+
variables?: KnowledgeDestination;
|
|
433
|
+
data?: KnowledgeToolResult;
|
|
434
|
+
error: unknown;
|
|
435
|
+
isPending: boolean;
|
|
436
|
+
},
|
|
437
|
+
destination: KnowledgeDestination | null,
|
|
438
|
+
) {
|
|
439
|
+
return sameDestination(mutation.variables, destination)
|
|
440
|
+
? { result: mutation.data, error: mutation.error, pending: mutation.isPending }
|
|
441
|
+
: { result: undefined, error: null, pending: false };
|
|
442
|
+
}
|
|
290
443
|
|
|
444
|
+
function SearchCard({ theme, hostId, destination }: WorkflowProps) {
|
|
445
|
+
const styles = createStyles(theme);
|
|
446
|
+
const searchKnowledge = useRpc(searchContextModeKnowledge);
|
|
447
|
+
const [query, setQuery] = useState("");
|
|
448
|
+
const [source, setSource] = useState("");
|
|
449
|
+
const [scope, setScope] = useState<SearchScope>("worktree");
|
|
450
|
+
const [sort, setSort] = useState<SearchSort>("relevance");
|
|
451
|
+
const [extraPages, setExtraPages] = useState<SearchKnowledgeResult[]>([]);
|
|
291
452
|
const search = useMutation({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
...(source ? { source } : {}),
|
|
301
|
-
});
|
|
302
|
-
},
|
|
453
|
+
mutationKey: knowledgeMutationKey(hostId),
|
|
454
|
+
mutationFn: (request: SearchKnowledgeInput) => searchKnowledge(request),
|
|
455
|
+
onMutate: () => setExtraPages([]),
|
|
456
|
+
});
|
|
457
|
+
const moreResults = useMutation({
|
|
458
|
+
mutationKey: knowledgeMutationKey(hostId),
|
|
459
|
+
mutationFn: ({ request, page }: MoreResultsInput) => searchKnowledge({ ...request, page }),
|
|
460
|
+
onSuccess: (result) => setExtraPages((pages) => [...pages, result]),
|
|
303
461
|
});
|
|
462
|
+
const request = search.variables;
|
|
463
|
+
// Results stay with the project they came from; editing the path hides them but keeps the form.
|
|
464
|
+
const current = sameDestination(request, destination);
|
|
465
|
+
const lastPage = extraPages.at(-1) ?? search.data;
|
|
466
|
+
const nextPage = lastPage?.next ?? null;
|
|
467
|
+
const trimmedQuery = query.trim();
|
|
468
|
+
const canSearch = destination !== null && trimmedQuery.length > 0;
|
|
469
|
+
|
|
470
|
+
function startSearch() {
|
|
471
|
+
if (!destination) return;
|
|
472
|
+
const trimmedSource = source.trim();
|
|
473
|
+
moreResults.reset();
|
|
474
|
+
search.mutate({
|
|
475
|
+
...destination,
|
|
476
|
+
queries: [trimmedQuery],
|
|
477
|
+
scope,
|
|
478
|
+
sort,
|
|
479
|
+
...(trimmedSource ? { source: trimmedSource } : {}),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function loadMore() {
|
|
484
|
+
if (request && nextPage) moreResults.mutate({ request, page: nextPage });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<WorkflowCard
|
|
489
|
+
theme={theme}
|
|
490
|
+
title="Search"
|
|
491
|
+
description="Search indexed knowledge. Add a source filter when you only want matches from one named source."
|
|
492
|
+
>
|
|
493
|
+
<Field
|
|
494
|
+
theme={theme}
|
|
495
|
+
accessibilityLabel="Knowledge search query"
|
|
496
|
+
label="Query"
|
|
497
|
+
maxLength={1_024}
|
|
498
|
+
onChangeText={setQuery}
|
|
499
|
+
placeholder="What do you need to find?"
|
|
500
|
+
value={query}
|
|
501
|
+
/>
|
|
502
|
+
<Field
|
|
503
|
+
theme={theme}
|
|
504
|
+
accessibilityLabel="Knowledge search source filter"
|
|
505
|
+
label="Source filter (optional)"
|
|
506
|
+
maxLength={512}
|
|
507
|
+
onChangeText={setSource}
|
|
508
|
+
placeholder="docs, repository, or source label"
|
|
509
|
+
value={source}
|
|
510
|
+
/>
|
|
511
|
+
<LabeledChoices<SearchScope>
|
|
512
|
+
theme={theme}
|
|
513
|
+
label="Scope"
|
|
514
|
+
accessibilityLabel={(option) => `Search ${option.label.toLowerCase()} scope`}
|
|
515
|
+
onChange={setScope}
|
|
516
|
+
options={SEARCH_SCOPES}
|
|
517
|
+
value={scope}
|
|
518
|
+
/>
|
|
519
|
+
<LabeledChoices<SearchSort>
|
|
520
|
+
theme={theme}
|
|
521
|
+
label="Order"
|
|
522
|
+
accessibilityLabel={(option) => `Order results by ${option.label.toLowerCase()}`}
|
|
523
|
+
onChange={setSort}
|
|
524
|
+
options={SEARCH_SORTS}
|
|
525
|
+
value={sort}
|
|
526
|
+
/>
|
|
527
|
+
<ActionButton
|
|
528
|
+
theme={theme}
|
|
529
|
+
variant="primary"
|
|
530
|
+
disabled={!canSearch || search.isPending || moreResults.isPending}
|
|
531
|
+
label={search.isPending ? "Searching..." : "Search knowledge"}
|
|
532
|
+
onPress={startSearch}
|
|
533
|
+
/>
|
|
534
|
+
<ResultPanel
|
|
535
|
+
theme={theme}
|
|
536
|
+
emptyLabel="No matching knowledge"
|
|
537
|
+
{...forDestination(search, destination)}
|
|
538
|
+
source={request?.source}
|
|
539
|
+
/>
|
|
540
|
+
{current
|
|
541
|
+
? extraPages.map((page, pageIndex) => (
|
|
542
|
+
<View
|
|
543
|
+
key={`${page.completedAt}:${pageIndex}`}
|
|
544
|
+
accessibilityLiveRegion="polite"
|
|
545
|
+
style={styles.result}
|
|
546
|
+
>
|
|
547
|
+
<Text style={styles.metaLabel}>Page {pageIndex + 2}</Text>
|
|
548
|
+
<OutputFrame theme={theme} output={page.output} emptyLabel="No more matches" />
|
|
549
|
+
</View>
|
|
550
|
+
))
|
|
551
|
+
: null}
|
|
552
|
+
{current && nextPage && !search.isPending ? (
|
|
553
|
+
<ActionButton
|
|
554
|
+
theme={theme}
|
|
555
|
+
accessibilityLabel="Load more search results"
|
|
556
|
+
disabled={moreResults.isPending}
|
|
557
|
+
label={moreResults.isPending ? "Loading..." : "More results"}
|
|
558
|
+
onPress={loadMore}
|
|
559
|
+
/>
|
|
560
|
+
) : null}
|
|
561
|
+
{current ? <ErrorText theme={theme} error={moreResults.error} /> : null}
|
|
562
|
+
</WorkflowCard>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function SourceManagement(props: WorkflowProps) {
|
|
567
|
+
return (
|
|
568
|
+
<>
|
|
569
|
+
<IndexPathCard {...props} />
|
|
570
|
+
<IndexUrlCard {...props} />
|
|
571
|
+
<PurgeCard {...props} />
|
|
572
|
+
</>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function IndexPathCard({ theme, hostId, destination }: WorkflowProps) {
|
|
577
|
+
const indexPath = useRpc(indexContextModePath);
|
|
578
|
+
const refreshAnalytics = useRefreshAnalytics(hostId);
|
|
579
|
+
const [path, setPath] = useState("");
|
|
304
580
|
const index = useMutation({
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const source = pathSource.trim();
|
|
308
|
-
setPathResultSource(source || path.trim());
|
|
309
|
-
return indexPath({
|
|
310
|
-
provider,
|
|
311
|
-
projectPath: projectPath.trim(),
|
|
312
|
-
path: path.trim(),
|
|
313
|
-
...(source ? { source } : {}),
|
|
314
|
-
});
|
|
315
|
-
},
|
|
581
|
+
mutationKey: knowledgeMutationKey(hostId),
|
|
582
|
+
mutationFn: (input: IndexPathInput) => indexPath(input),
|
|
316
583
|
onSuccess: refreshAnalytics,
|
|
317
584
|
});
|
|
585
|
+
const target = path.trim();
|
|
586
|
+
|
|
587
|
+
function submit() {
|
|
588
|
+
if (destination) index.mutate({ ...destination, path: target });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<WorkflowCard
|
|
593
|
+
theme={theme}
|
|
594
|
+
title="Index a local path"
|
|
595
|
+
description="Add a file or directory inside the selected project."
|
|
596
|
+
>
|
|
597
|
+
<Field
|
|
598
|
+
theme={theme}
|
|
599
|
+
accessibilityLabel="Local path to index"
|
|
600
|
+
label="Local path"
|
|
601
|
+
maxLength={4_096}
|
|
602
|
+
onChangeText={setPath}
|
|
603
|
+
placeholder="/absolute/path/to/docs"
|
|
604
|
+
value={path}
|
|
605
|
+
/>
|
|
606
|
+
<ActionButton
|
|
607
|
+
theme={theme}
|
|
608
|
+
disabled={!destination || !target || index.isPending}
|
|
609
|
+
label={index.isPending ? "Indexing..." : "Index path"}
|
|
610
|
+
onPress={submit}
|
|
611
|
+
/>
|
|
612
|
+
<ResultPanel
|
|
613
|
+
theme={theme}
|
|
614
|
+
emptyLabel="No report"
|
|
615
|
+
{...forDestination(index, destination)}
|
|
616
|
+
source={index.variables?.path}
|
|
617
|
+
/>
|
|
618
|
+
</WorkflowCard>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function IndexUrlCard({ theme, hostId, destination }: WorkflowProps) {
|
|
623
|
+
const fetchAndIndex = useRpc(fetchAndIndexContextMode);
|
|
624
|
+
const refreshAnalytics = useRefreshAnalytics(hostId);
|
|
625
|
+
const [url, setUrl] = useState("");
|
|
318
626
|
const fetch = useMutation({
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const source = urlSource.trim();
|
|
322
|
-
setUrlResultSource(source || url.trim());
|
|
323
|
-
return fetchAndIndex({
|
|
324
|
-
provider,
|
|
325
|
-
projectPath: projectPath.trim(),
|
|
326
|
-
url: url.trim(),
|
|
327
|
-
...(source ? { source } : {}),
|
|
328
|
-
});
|
|
329
|
-
},
|
|
627
|
+
mutationKey: knowledgeMutationKey(hostId),
|
|
628
|
+
mutationFn: (input: FetchAndIndexInput) => fetchAndIndex(input),
|
|
330
629
|
onSuccess: refreshAnalytics,
|
|
331
630
|
});
|
|
631
|
+
const target = url.trim();
|
|
632
|
+
|
|
633
|
+
function submit() {
|
|
634
|
+
if (destination) fetch.mutate({ ...destination, url: target });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return (
|
|
638
|
+
<WorkflowCard
|
|
639
|
+
theme={theme}
|
|
640
|
+
title="Index a URL"
|
|
641
|
+
description="Fetch one public HTTP or HTTPS page and add it to the selected project store."
|
|
642
|
+
>
|
|
643
|
+
<Field
|
|
644
|
+
theme={theme}
|
|
645
|
+
accessibilityLabel="URL to fetch and index"
|
|
646
|
+
label="URL"
|
|
647
|
+
maxLength={4_096}
|
|
648
|
+
onChangeText={setUrl}
|
|
649
|
+
placeholder="https://docs.example.com/guide"
|
|
650
|
+
value={url}
|
|
651
|
+
/>
|
|
652
|
+
<ActionButton
|
|
653
|
+
theme={theme}
|
|
654
|
+
disabled={!destination || !target || fetch.isPending}
|
|
655
|
+
label={fetch.isPending ? "Indexing..." : "Index URL"}
|
|
656
|
+
onPress={submit}
|
|
657
|
+
/>
|
|
658
|
+
<ResultPanel
|
|
659
|
+
theme={theme}
|
|
660
|
+
emptyLabel="No report"
|
|
661
|
+
{...forDestination(fetch, destination)}
|
|
662
|
+
source={fetch.variables?.url}
|
|
663
|
+
/>
|
|
664
|
+
</WorkflowCard>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function purgeTargetFor(
|
|
669
|
+
scope: PurgeScope | null,
|
|
670
|
+
sessionId: string,
|
|
671
|
+
source: string,
|
|
672
|
+
): PurgeTarget | null {
|
|
673
|
+
if (scope === "project") return { scope };
|
|
674
|
+
if (scope === "session" && sessionId) return { scope, sessionId };
|
|
675
|
+
if (scope === "source" && source) return { scope, source };
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function PurgeCard({ theme, hostId, destination }: WorkflowProps) {
|
|
680
|
+
const purgeKnowledge = useRpc(purgeContextModeKnowledge);
|
|
681
|
+
const refreshAnalytics = useRefreshAnalytics(hostId);
|
|
682
|
+
const [scope, setScope] = useState<PurgeScope | null>(null);
|
|
683
|
+
const [sessionId, setSessionId] = useState("");
|
|
684
|
+
const [source, setSource] = useState("");
|
|
685
|
+
// The armed review names one exact destination and target; editing either disarms it.
|
|
686
|
+
const [armedFor, setArmedFor] = useState<string | null>(null);
|
|
332
687
|
const purge = useMutation({
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (purgeScope === "session") {
|
|
336
|
-
return purgeKnowledge({
|
|
337
|
-
provider,
|
|
338
|
-
projectPath: projectPath.trim(),
|
|
339
|
-
confirm: true,
|
|
340
|
-
scope: "session",
|
|
341
|
-
sessionId: sessionId.trim(),
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
return purgeKnowledge({
|
|
345
|
-
provider,
|
|
346
|
-
projectPath: projectPath.trim(),
|
|
347
|
-
confirm: true,
|
|
348
|
-
scope: "project",
|
|
349
|
-
});
|
|
350
|
-
},
|
|
688
|
+
mutationKey: knowledgeMutationKey(hostId),
|
|
689
|
+
mutationFn: (input: PurgeKnowledgeInput) => purgeKnowledge(input),
|
|
351
690
|
onSuccess: refreshAnalytics,
|
|
352
|
-
onSettled: () =>
|
|
691
|
+
onSettled: () => setArmedFor(null),
|
|
353
692
|
});
|
|
693
|
+
const target = purgeTargetFor(scope, sessionId.trim(), source.trim());
|
|
694
|
+
const reviewKey = JSON.stringify([destination, target]);
|
|
695
|
+
const armed = armedFor === reviewKey;
|
|
354
696
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
setProvider(next);
|
|
358
|
-
setPurgeArmed(false);
|
|
359
|
-
search.reset();
|
|
360
|
-
index.reset();
|
|
361
|
-
fetch.reset();
|
|
362
|
-
purge.reset();
|
|
363
|
-
};
|
|
364
|
-
const changePurgeScope = (next: PurgeScope) => {
|
|
365
|
-
setPurgeScope(next);
|
|
366
|
-
setPurgeArmed(false);
|
|
697
|
+
function changeScope(next: PurgeScope) {
|
|
698
|
+
setScope(next);
|
|
367
699
|
purge.reset();
|
|
368
|
-
}
|
|
369
|
-
const providerReady = provider !== null && projectPath.trim().length > 0;
|
|
370
|
-
const canPreparePurge =
|
|
371
|
-
providerReady &&
|
|
372
|
-
purgeScope !== null &&
|
|
373
|
-
(purgeScope !== "session" || sessionId.trim().length > 0);
|
|
374
|
-
|
|
375
|
-
return (
|
|
376
|
-
<View style={styles.section}>
|
|
377
|
-
<View style={styles.sectionHeader}>
|
|
378
|
-
<Text accessibilityRole="header" style={styles.sectionTitle}>
|
|
379
|
-
Knowledge
|
|
380
|
-
</Text>
|
|
381
|
-
<Text style={styles.muted}>
|
|
382
|
-
Search or add material in one provider-specific store. Nothing is copied between
|
|
383
|
-
providers.
|
|
384
|
-
</Text>
|
|
385
|
-
</View>
|
|
700
|
+
}
|
|
386
701
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
<View accessibilityRole="radiogroup" style={styles.choiceRow}>
|
|
391
|
-
{providers.map((id) => {
|
|
392
|
-
const selected = provider === id;
|
|
393
|
-
return (
|
|
394
|
-
<Pressable
|
|
395
|
-
key={id}
|
|
396
|
-
accessibilityLabel={`${providerLabel(id)} provider store`}
|
|
397
|
-
disabled={busy}
|
|
398
|
-
accessibilityRole="radio"
|
|
399
|
-
accessibilityState={{ checked: selected, disabled: busy }}
|
|
400
|
-
onPress={() => changeProvider(id)}
|
|
401
|
-
style={[
|
|
402
|
-
styles.choice,
|
|
403
|
-
selected ? styles.choiceSelected : null,
|
|
404
|
-
busy ? styles.disabled : null,
|
|
405
|
-
]}
|
|
406
|
-
>
|
|
407
|
-
<Text style={[styles.choiceText, selected ? styles.choiceTextSelected : null]}>
|
|
408
|
-
{providerLabel(id)}
|
|
409
|
-
</Text>
|
|
410
|
-
</Pressable>
|
|
411
|
-
);
|
|
412
|
-
})}
|
|
413
|
-
</View>
|
|
414
|
-
<Field
|
|
415
|
-
accessibilityLabel="Knowledge project path"
|
|
416
|
-
label="Project path"
|
|
417
|
-
maxLength={4_096}
|
|
418
|
-
onChangeText={(value) => {
|
|
419
|
-
setProjectPath(value);
|
|
420
|
-
setPurgeArmed(false);
|
|
421
|
-
}}
|
|
422
|
-
placeholder="/absolute/path/to/project"
|
|
423
|
-
styles={styles}
|
|
424
|
-
value={projectPath}
|
|
425
|
-
/>
|
|
426
|
-
<Text accessibilityLiveRegion="polite" style={styles.scopeNote}>
|
|
427
|
-
{provider && projectPath.trim()
|
|
428
|
-
? `Current destination: ${providerLabel(provider)} storage for ${projectPath.trim()}.`
|
|
429
|
-
: "Select a provider and absolute project path. No storage destination is selected by default."}
|
|
430
|
-
</Text>
|
|
431
|
-
</View>
|
|
702
|
+
function confirm() {
|
|
703
|
+
if (destination && target) purge.mutate({ ...destination, confirm: true, ...target });
|
|
704
|
+
}
|
|
432
705
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
706
|
+
return (
|
|
707
|
+
<WorkflowCard
|
|
708
|
+
theme={theme}
|
|
709
|
+
title="Purge knowledge"
|
|
710
|
+
description="Purge part of the selected project store for every agent. Select a scope, review it, then confirm."
|
|
711
|
+
>
|
|
712
|
+
<LabeledChoices
|
|
713
|
+
theme={theme}
|
|
714
|
+
label="Scope"
|
|
715
|
+
accessibilityLabel={(option) => `Purge ${option.label.toLowerCase()} scope`}
|
|
716
|
+
onChange={changeScope}
|
|
717
|
+
options={PURGE_SCOPES}
|
|
718
|
+
value={scope}
|
|
719
|
+
/>
|
|
720
|
+
{scope === "session" ? (
|
|
438
721
|
<Field
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
value={
|
|
722
|
+
theme={theme}
|
|
723
|
+
accessibilityLabel="Session identifier to purge"
|
|
724
|
+
label="Session ID"
|
|
725
|
+
maxLength={256}
|
|
726
|
+
onChangeText={setSessionId}
|
|
727
|
+
placeholder="Exact Context Mode session ID"
|
|
728
|
+
value={sessionId}
|
|
446
729
|
/>
|
|
730
|
+
) : null}
|
|
731
|
+
{scope === "source" ? (
|
|
447
732
|
<Field
|
|
448
|
-
|
|
449
|
-
|
|
733
|
+
theme={theme}
|
|
734
|
+
accessibilityLabel="Source label to forget"
|
|
735
|
+
label="Source label"
|
|
450
736
|
maxLength={512}
|
|
451
|
-
onChangeText={
|
|
452
|
-
placeholder="
|
|
453
|
-
|
|
454
|
-
value={searchSource}
|
|
737
|
+
onChangeText={setSource}
|
|
738
|
+
placeholder="Exact indexed source label"
|
|
739
|
+
value={source}
|
|
455
740
|
/>
|
|
456
|
-
<View style={styles.actions}>
|
|
457
|
-
<ActionButton
|
|
458
|
-
disabled={!providerReady || query.trim().length === 0 || search.isPending}
|
|
459
|
-
label={search.isPending ? "Searching…" : "Search knowledge"}
|
|
460
|
-
onPress={() => search.mutate()}
|
|
461
|
-
primary
|
|
462
|
-
styles={styles}
|
|
463
|
-
/>
|
|
464
|
-
</View>
|
|
465
|
-
<ResultPanel
|
|
466
|
-
emptyLabel="No matching knowledge was returned."
|
|
467
|
-
error={search.error}
|
|
468
|
-
pending={search.isPending}
|
|
469
|
-
result={search.data}
|
|
470
|
-
source={searchResultSource || undefined}
|
|
471
|
-
styles={styles}
|
|
472
|
-
/>
|
|
473
|
-
</WorkflowCard>
|
|
474
|
-
|
|
475
|
-
<Pressable
|
|
476
|
-
accessibilityLabel={managementExpanded ? "Hide source management" : "Add or manage sources"}
|
|
477
|
-
accessibilityRole="button"
|
|
478
|
-
accessibilityState={{ expanded: managementExpanded }}
|
|
479
|
-
onPress={() => setManagementExpanded((expanded) => !expanded)}
|
|
480
|
-
style={styles.button}
|
|
481
|
-
>
|
|
482
|
-
<Text style={styles.buttonText}>
|
|
483
|
-
{managementExpanded ? "Hide source management" : "Add or manage sources"}
|
|
484
|
-
</Text>
|
|
485
|
-
</Pressable>
|
|
486
|
-
|
|
487
|
-
{managementExpanded ? (
|
|
488
|
-
<>
|
|
489
|
-
<WorkflowCard
|
|
490
|
-
title="Source visibility"
|
|
491
|
-
description="Open Context Mode Insight to inspect indexed sources and understand what is available to search."
|
|
492
|
-
styles={styles}
|
|
493
|
-
>
|
|
494
|
-
<View style={styles.actions}>
|
|
495
|
-
<ExternalLink
|
|
496
|
-
accessibilityLabel="Open Context Mode Insight in a browser"
|
|
497
|
-
href={INSIGHT_URL}
|
|
498
|
-
onError={(error) =>
|
|
499
|
-
setLinkError(error instanceof Error ? error.message : String(error))
|
|
500
|
-
}
|
|
501
|
-
>
|
|
502
|
-
<View style={styles.linkButton}>
|
|
503
|
-
<Text style={styles.linkText}>Open Insight</Text>
|
|
504
|
-
</View>
|
|
505
|
-
</ExternalLink>
|
|
506
|
-
</View>
|
|
507
|
-
{linkError ? (
|
|
508
|
-
<Text accessibilityRole="alert" style={styles.error}>
|
|
509
|
-
Could not open Insight: {linkError}
|
|
510
|
-
</Text>
|
|
511
|
-
) : null}
|
|
512
|
-
</WorkflowCard>
|
|
513
|
-
|
|
514
|
-
<WorkflowCard
|
|
515
|
-
title="Index a local path"
|
|
516
|
-
description="Add a file or directory from the selected provider environment. The source label stays visible with the indexed material."
|
|
517
|
-
styles={styles}
|
|
518
|
-
>
|
|
519
|
-
<Field
|
|
520
|
-
accessibilityLabel="Local path to index"
|
|
521
|
-
label="Local path"
|
|
522
|
-
maxLength={4_096}
|
|
523
|
-
onChangeText={setPath}
|
|
524
|
-
placeholder="/absolute/path/to/docs"
|
|
525
|
-
styles={styles}
|
|
526
|
-
value={path}
|
|
527
|
-
/>
|
|
528
|
-
<Field
|
|
529
|
-
accessibilityLabel="Local path source label"
|
|
530
|
-
label="Source label (optional)"
|
|
531
|
-
maxLength={512}
|
|
532
|
-
onChangeText={setPathSource}
|
|
533
|
-
placeholder="project-docs"
|
|
534
|
-
styles={styles}
|
|
535
|
-
value={pathSource}
|
|
536
|
-
/>
|
|
537
|
-
<View style={styles.actions}>
|
|
538
|
-
<ActionButton
|
|
539
|
-
disabled={!providerReady || path.trim().length === 0 || index.isPending}
|
|
540
|
-
label={index.isPending ? "Indexing…" : "Index path"}
|
|
541
|
-
onPress={() => index.mutate()}
|
|
542
|
-
styles={styles}
|
|
543
|
-
/>
|
|
544
|
-
</View>
|
|
545
|
-
<ResultPanel
|
|
546
|
-
emptyLabel="Context Mode completed without a text report."
|
|
547
|
-
error={index.error}
|
|
548
|
-
pending={index.isPending}
|
|
549
|
-
result={index.data}
|
|
550
|
-
source={pathResultSource || undefined}
|
|
551
|
-
styles={styles}
|
|
552
|
-
/>
|
|
553
|
-
</WorkflowCard>
|
|
554
|
-
|
|
555
|
-
<WorkflowCard
|
|
556
|
-
title="Fetch and index a URL"
|
|
557
|
-
description="Fetch one HTTP or HTTPS source and add it to the selected provider store."
|
|
558
|
-
styles={styles}
|
|
559
|
-
>
|
|
560
|
-
<Field
|
|
561
|
-
accessibilityLabel="URL to fetch and index"
|
|
562
|
-
label="URL"
|
|
563
|
-
maxLength={4_096}
|
|
564
|
-
onChangeText={setUrl}
|
|
565
|
-
placeholder="https://docs.example.com/guide"
|
|
566
|
-
styles={styles}
|
|
567
|
-
value={url}
|
|
568
|
-
/>
|
|
569
|
-
<Field
|
|
570
|
-
accessibilityLabel="Fetched URL source label"
|
|
571
|
-
label="Source label (optional)"
|
|
572
|
-
maxLength={512}
|
|
573
|
-
onChangeText={setUrlSource}
|
|
574
|
-
placeholder="product-guide"
|
|
575
|
-
styles={styles}
|
|
576
|
-
value={urlSource}
|
|
577
|
-
/>
|
|
578
|
-
<View style={styles.actions}>
|
|
579
|
-
<ActionButton
|
|
580
|
-
disabled={!providerReady || url.trim().length === 0 || fetch.isPending}
|
|
581
|
-
label={fetch.isPending ? "Fetching…" : "Fetch and index"}
|
|
582
|
-
onPress={() => fetch.mutate()}
|
|
583
|
-
styles={styles}
|
|
584
|
-
/>
|
|
585
|
-
</View>
|
|
586
|
-
<ResultPanel
|
|
587
|
-
emptyLabel="Context Mode completed without a text report."
|
|
588
|
-
error={fetch.error}
|
|
589
|
-
pending={fetch.isPending}
|
|
590
|
-
result={fetch.data}
|
|
591
|
-
source={urlResultSource || undefined}
|
|
592
|
-
styles={styles}
|
|
593
|
-
/>
|
|
594
|
-
</WorkflowCard>
|
|
595
|
-
|
|
596
|
-
<WorkflowCard
|
|
597
|
-
title="Purge knowledge"
|
|
598
|
-
description="Purge only the selected provider store and scope. Select a scope, review it, then confirm."
|
|
599
|
-
styles={styles}
|
|
600
|
-
>
|
|
601
|
-
<Text style={styles.fieldLabel}>Scope</Text>
|
|
602
|
-
<View accessibilityRole="radiogroup" style={styles.choiceRow}>
|
|
603
|
-
{(["project", "session"] as const).map((scope) => {
|
|
604
|
-
const selected = purgeScope === scope;
|
|
605
|
-
const label = scope === "project" ? "Project" : "Session";
|
|
606
|
-
return (
|
|
607
|
-
<Pressable
|
|
608
|
-
key={scope}
|
|
609
|
-
accessibilityLabel={`Purge ${label.toLowerCase()} scope`}
|
|
610
|
-
accessibilityRole="radio"
|
|
611
|
-
accessibilityState={{ checked: selected }}
|
|
612
|
-
onPress={() => changePurgeScope(scope)}
|
|
613
|
-
style={[styles.choice, selected ? styles.choiceSelected : null]}
|
|
614
|
-
>
|
|
615
|
-
<Text style={[styles.choiceText, selected ? styles.choiceTextSelected : null]}>
|
|
616
|
-
{label}
|
|
617
|
-
</Text>
|
|
618
|
-
</Pressable>
|
|
619
|
-
);
|
|
620
|
-
})}
|
|
621
|
-
</View>
|
|
622
|
-
{purgeScope === "session" ? (
|
|
623
|
-
<Field
|
|
624
|
-
accessibilityLabel="Session identifier to purge"
|
|
625
|
-
label="Session ID"
|
|
626
|
-
maxLength={256}
|
|
627
|
-
onChangeText={(value) => {
|
|
628
|
-
setSessionId(value);
|
|
629
|
-
setPurgeArmed(false);
|
|
630
|
-
}}
|
|
631
|
-
placeholder="Exact Context Mode session ID"
|
|
632
|
-
styles={styles}
|
|
633
|
-
value={sessionId}
|
|
634
|
-
/>
|
|
635
|
-
) : null}
|
|
636
|
-
{!purgeArmed ? (
|
|
637
|
-
<View style={styles.actions}>
|
|
638
|
-
<ActionButton
|
|
639
|
-
danger
|
|
640
|
-
disabled={!canPreparePurge || purge.isPending}
|
|
641
|
-
label="Review purge"
|
|
642
|
-
onPress={() => setPurgeArmed(true)}
|
|
643
|
-
styles={styles}
|
|
644
|
-
/>
|
|
645
|
-
</View>
|
|
646
|
-
) : (
|
|
647
|
-
<View accessibilityRole="alert" style={styles.confirmation}>
|
|
648
|
-
<Text style={styles.confirmationTitle}>Confirm permanent purge</Text>
|
|
649
|
-
<Text style={styles.muted}>
|
|
650
|
-
{`Delete ${purgeScope === "session" ? `session ${sessionId.trim()}` : "project"} knowledge from ${provider} storage? This does not affect other providers.`}
|
|
651
|
-
</Text>
|
|
652
|
-
<View style={styles.actions}>
|
|
653
|
-
<ActionButton
|
|
654
|
-
danger
|
|
655
|
-
label={purge.isPending ? "Purging…" : "Confirm purge"}
|
|
656
|
-
onPress={() => purge.mutate()}
|
|
657
|
-
styles={styles}
|
|
658
|
-
/>
|
|
659
|
-
<ActionButton
|
|
660
|
-
disabled={purge.isPending}
|
|
661
|
-
label="Cancel"
|
|
662
|
-
onPress={() => setPurgeArmed(false)}
|
|
663
|
-
styles={styles}
|
|
664
|
-
/>
|
|
665
|
-
</View>
|
|
666
|
-
</View>
|
|
667
|
-
)}
|
|
668
|
-
<ResultPanel
|
|
669
|
-
emptyLabel="The selected knowledge scope was purged."
|
|
670
|
-
error={purge.error}
|
|
671
|
-
pending={purge.isPending}
|
|
672
|
-
result={purge.data}
|
|
673
|
-
styles={styles}
|
|
674
|
-
/>
|
|
675
|
-
</WorkflowCard>
|
|
676
|
-
</>
|
|
677
741
|
) : null}
|
|
742
|
+
{armed && destination && target ? (
|
|
743
|
+
<ConfirmPanel
|
|
744
|
+
theme={theme}
|
|
745
|
+
message={`Delete ${describePurgeTarget(target)} from the ${destination.projectPath} store? ${PURGE_CONSEQUENCE[target.scope]}`}
|
|
746
|
+
confirmLabel="Confirm purge"
|
|
747
|
+
pendingLabel="Purging..."
|
|
748
|
+
pending={purge.isPending}
|
|
749
|
+
onConfirm={confirm}
|
|
750
|
+
onCancel={() => setArmedFor(null)}
|
|
751
|
+
/>
|
|
752
|
+
) : (
|
|
753
|
+
<ActionButton
|
|
754
|
+
theme={theme}
|
|
755
|
+
variant="outline"
|
|
756
|
+
disabled={!destination || !target || purge.isPending}
|
|
757
|
+
label="Review purge"
|
|
758
|
+
onPress={() => setArmedFor(reviewKey)}
|
|
759
|
+
/>
|
|
760
|
+
)}
|
|
761
|
+
<ResultPanel theme={theme} emptyLabel="Purged" {...forDestination(purge, destination)} />
|
|
762
|
+
</WorkflowCard>
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
interface ForgetButtonProps extends Pick<PluginSurfaceProps, "theme" | "host"> {
|
|
767
|
+
provider: string;
|
|
768
|
+
projectPath: string;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
interface ForgetProps extends ForgetButtonProps {
|
|
772
|
+
target: ForgetTarget;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Per-row "Forget" with the same review-then-confirm step as purge. */
|
|
776
|
+
function ForgetButton({ theme, host, provider, projectPath, target }: ForgetProps) {
|
|
777
|
+
const purgeKnowledge = useRpc(purgeContextModeKnowledge);
|
|
778
|
+
const refreshAnalytics = useRefreshAnalytics(host.id);
|
|
779
|
+
const [armed, setArmed] = useState(false);
|
|
780
|
+
const forget = useMutation({
|
|
781
|
+
mutationKey: knowledgeMutationKey(host.id),
|
|
782
|
+
mutationFn: (input: PurgeKnowledgeInput) => purgeKnowledge(input),
|
|
783
|
+
onSuccess: refreshAnalytics,
|
|
784
|
+
onSettled: () => setArmed(false),
|
|
785
|
+
});
|
|
786
|
+
const targetLabel = describePurgeTarget(target);
|
|
787
|
+
const mutedStyle = createStyles(theme).muted;
|
|
788
|
+
|
|
789
|
+
if (forget.isSuccess) return <Text style={mutedStyle}>Forgotten</Text>;
|
|
790
|
+
return (
|
|
791
|
+
<View style={{ gap: 4 }}>
|
|
792
|
+
{armed ? (
|
|
793
|
+
<ConfirmPanel
|
|
794
|
+
theme={theme}
|
|
795
|
+
message={`Forget ${targetLabel} from the ${projectPath} store? ${PURGE_CONSEQUENCE[target.scope]}`}
|
|
796
|
+
confirmAccessibilityLabel={`Confirm forget ${targetLabel}`}
|
|
797
|
+
confirmLabel="Confirm forget"
|
|
798
|
+
pendingLabel="Forgetting..."
|
|
799
|
+
pending={forget.isPending}
|
|
800
|
+
onConfirm={() => forget.mutate({ provider, projectPath, confirm: true, ...target })}
|
|
801
|
+
onCancel={() => setArmed(false)}
|
|
802
|
+
/>
|
|
803
|
+
) : (
|
|
804
|
+
<ActionButton
|
|
805
|
+
theme={theme}
|
|
806
|
+
variant="outline"
|
|
807
|
+
accessibilityLabel={`Forget ${targetLabel}`}
|
|
808
|
+
label="Forget"
|
|
809
|
+
onPress={() => setArmed(true)}
|
|
810
|
+
/>
|
|
811
|
+
)}
|
|
812
|
+
<ErrorText theme={theme} error={forget.error} />
|
|
678
813
|
</View>
|
|
679
814
|
);
|
|
680
815
|
}
|
|
681
816
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
backgroundColor: theme.colors.surface1,
|
|
699
|
-
},
|
|
700
|
-
cardHeader: { gap: 3 },
|
|
701
|
-
cardTitle: { color: theme.colors.foreground, fontSize: 15, fontWeight: "700" as const },
|
|
702
|
-
muted: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
703
|
-
scopeNote: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
704
|
-
field: { gap: 5 },
|
|
705
|
-
fieldLabel: {
|
|
706
|
-
color: theme.colors.foregroundMuted,
|
|
707
|
-
fontSize: 12,
|
|
708
|
-
fontWeight: "600" as const,
|
|
709
|
-
},
|
|
710
|
-
input: {
|
|
711
|
-
minHeight: 40,
|
|
712
|
-
width: "100%" as const,
|
|
713
|
-
borderWidth: 1,
|
|
714
|
-
borderColor: theme.colors.border,
|
|
715
|
-
borderRadius: 8,
|
|
716
|
-
paddingHorizontal: 11,
|
|
717
|
-
paddingVertical: 8,
|
|
718
|
-
color: theme.colors.foreground,
|
|
719
|
-
backgroundColor: theme.colors.surface0,
|
|
720
|
-
fontSize: 14,
|
|
721
|
-
},
|
|
722
|
-
placeholder: { color: theme.colors.foregroundMuted },
|
|
723
|
-
choiceRow: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 7 },
|
|
724
|
-
choice: {
|
|
725
|
-
minHeight: 36,
|
|
726
|
-
justifyContent: "center" as const,
|
|
727
|
-
paddingHorizontal: 11,
|
|
728
|
-
paddingVertical: 7,
|
|
729
|
-
borderWidth: 1,
|
|
730
|
-
borderColor: theme.colors.border,
|
|
731
|
-
borderRadius: 999,
|
|
732
|
-
backgroundColor: theme.colors.surface0,
|
|
733
|
-
},
|
|
734
|
-
choiceSelected: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
|
|
735
|
-
choiceText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" as const },
|
|
736
|
-
choiceTextSelected: { color: theme.colors.accentForeground },
|
|
737
|
-
actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 8 },
|
|
738
|
-
button: {
|
|
739
|
-
minHeight: 38,
|
|
740
|
-
alignItems: "center" as const,
|
|
741
|
-
justifyContent: "center" as const,
|
|
742
|
-
paddingHorizontal: 13,
|
|
743
|
-
paddingVertical: 8,
|
|
744
|
-
borderWidth: 1,
|
|
745
|
-
borderColor: theme.colors.border,
|
|
746
|
-
borderRadius: 8,
|
|
747
|
-
backgroundColor: theme.colors.surface2,
|
|
748
|
-
},
|
|
749
|
-
primaryButton: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
|
|
750
|
-
dangerButton: {
|
|
751
|
-
borderColor: theme.colors.statusDanger,
|
|
752
|
-
backgroundColor: theme.colors.surface1,
|
|
753
|
-
},
|
|
754
|
-
disabled: { opacity: 0.5 },
|
|
755
|
-
buttonText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" as const },
|
|
756
|
-
primaryButtonText: { color: theme.colors.accentForeground },
|
|
757
|
-
dangerButtonText: { color: theme.colors.statusDanger },
|
|
758
|
-
linkButton: {
|
|
759
|
-
minHeight: 38,
|
|
760
|
-
alignItems: "center" as const,
|
|
761
|
-
justifyContent: "center" as const,
|
|
762
|
-
paddingHorizontal: 13,
|
|
763
|
-
paddingVertical: 8,
|
|
764
|
-
borderWidth: 1,
|
|
765
|
-
borderColor: theme.colors.border,
|
|
766
|
-
borderRadius: 8,
|
|
767
|
-
backgroundColor: theme.colors.surface2,
|
|
768
|
-
},
|
|
769
|
-
linkText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" as const },
|
|
770
|
-
pendingRow: { flexDirection: "row" as const, alignItems: "center" as const, gap: 8 },
|
|
771
|
-
activity: { color: theme.colors.accent },
|
|
772
|
-
error: { color: theme.colors.statusDanger, fontSize: 12, lineHeight: 17 },
|
|
773
|
-
result: {
|
|
774
|
-
gap: 7,
|
|
775
|
-
padding: 10,
|
|
776
|
-
borderWidth: 1,
|
|
777
|
-
borderColor: theme.colors.border,
|
|
778
|
-
borderRadius: 8,
|
|
779
|
-
backgroundColor: theme.colors.surface0,
|
|
780
|
-
},
|
|
781
|
-
metaRow: { flexDirection: "row" as const, alignItems: "flex-start" as const, gap: 8 },
|
|
782
|
-
metaLabel: { width: 58, color: theme.colors.foregroundMuted, fontSize: 11 },
|
|
783
|
-
metaValue: { flex: 1, color: theme.colors.foreground, fontSize: 11 },
|
|
784
|
-
outputFrame: { maxHeight: compact ? 160 : 220 },
|
|
785
|
-
output: {
|
|
786
|
-
color: theme.colors.foreground,
|
|
787
|
-
fontFamily: "monospace",
|
|
788
|
-
fontSize: compact ? 11 : 12,
|
|
789
|
-
lineHeight: compact ? 16 : 18,
|
|
790
|
-
},
|
|
791
|
-
confirmation: {
|
|
792
|
-
gap: 9,
|
|
793
|
-
padding: 11,
|
|
794
|
-
borderWidth: 1,
|
|
795
|
-
borderColor: theme.colors.statusDanger,
|
|
796
|
-
borderRadius: 8,
|
|
797
|
-
backgroundColor: theme.colors.surface0,
|
|
798
|
-
},
|
|
799
|
-
confirmationTitle: {
|
|
800
|
-
color: theme.colors.statusDanger,
|
|
801
|
-
fontSize: 13,
|
|
802
|
-
fontWeight: "700" as const,
|
|
803
|
-
},
|
|
804
|
-
};
|
|
817
|
+
interface ForgetSourceButtonProps extends ForgetButtonProps {
|
|
818
|
+
source: string;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Forgets one indexed source from the project store. */
|
|
822
|
+
export function ForgetSourceButton({ source, ...props }: ForgetSourceButtonProps) {
|
|
823
|
+
return <ForgetButton {...props} target={{ scope: "source", source }} />;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
interface ForgetSessionButtonProps extends ForgetButtonProps {
|
|
827
|
+
sessionId: string;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Forgets one recorded session's events from the project store. */
|
|
831
|
+
export function ForgetSessionButton({ sessionId, ...props }: ForgetSessionButtonProps) {
|
|
832
|
+
return <ForgetButton {...props} target={{ scope: "session", sessionId }} />;
|
|
805
833
|
}
|