@omercnet/paseo-context-mode 3.1.3 → 3.1.4-next.434.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +8 -1747
- package/package.json +7 -7
- 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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
2
|
+
import { Loading } from "@getpaseo/plugin/client/ui";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Pressable, Text, View } from "react-native";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `primary`: the one accent action on a surface. `secondary`: the default. `outline`: a quiet row
|
|
8
|
+
* action. `destructive`: only the confirm button inside a confirmation.
|
|
9
|
+
*/
|
|
10
|
+
export type ButtonVariant = "primary" | "secondary" | "outline" | "destructive";
|
|
11
|
+
|
|
12
|
+
interface ActionButtonProps {
|
|
13
|
+
theme: PluginTheme;
|
|
14
|
+
label: string;
|
|
15
|
+
onPress(): void;
|
|
16
|
+
variant?: ButtonVariant;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
accessibilityLabel?: string;
|
|
19
|
+
/** Set for disclosure buttons so assistive tech hears the expanded state. */
|
|
20
|
+
expanded?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ButtonColors {
|
|
24
|
+
background: string;
|
|
25
|
+
border: string;
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buttonColors(theme: PluginTheme, variant: ButtonVariant): ButtonColors {
|
|
30
|
+
const { colors } = theme;
|
|
31
|
+
const byVariant: Record<ButtonVariant, ButtonColors> = {
|
|
32
|
+
primary: { background: colors.accent, border: colors.accent, text: colors.accentForeground },
|
|
33
|
+
secondary: { background: colors.surface2, border: colors.surface2, text: colors.foreground },
|
|
34
|
+
outline: { background: "transparent", border: colors.border, text: colors.foreground },
|
|
35
|
+
destructive: {
|
|
36
|
+
background: colors.statusDanger,
|
|
37
|
+
border: colors.statusDanger,
|
|
38
|
+
text: colors.accentForeground,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return byVariant[variant];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ActionButton({
|
|
45
|
+
theme,
|
|
46
|
+
label,
|
|
47
|
+
onPress,
|
|
48
|
+
variant = "secondary",
|
|
49
|
+
disabled = false,
|
|
50
|
+
accessibilityLabel,
|
|
51
|
+
expanded,
|
|
52
|
+
}: ActionButtonProps) {
|
|
53
|
+
const colors = buttonColors(theme, variant);
|
|
54
|
+
return (
|
|
55
|
+
<Pressable
|
|
56
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
57
|
+
accessibilityRole="button"
|
|
58
|
+
accessibilityState={{ disabled, expanded }}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onPress={onPress}
|
|
61
|
+
style={{
|
|
62
|
+
minHeight: 32,
|
|
63
|
+
alignItems: "center",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
alignSelf: "flex-start",
|
|
66
|
+
paddingHorizontal: 12,
|
|
67
|
+
paddingVertical: 4,
|
|
68
|
+
borderWidth: 1,
|
|
69
|
+
borderColor: colors.border,
|
|
70
|
+
borderRadius: 999,
|
|
71
|
+
backgroundColor: colors.background,
|
|
72
|
+
opacity: disabled ? 0.5 : 1,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<Text style={{ color: colors.text, fontSize: 13, fontWeight: "500" }}>{label}</Text>
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type StatusTone = "success" | "warning" | "danger" | "neutral";
|
|
81
|
+
|
|
82
|
+
function toneColor(theme: PluginTheme, tone: StatusTone): string {
|
|
83
|
+
const byTone: Record<StatusTone, string> = {
|
|
84
|
+
success: theme.colors.statusSuccess,
|
|
85
|
+
warning: theme.colors.statusWarning,
|
|
86
|
+
danger: theme.colors.statusDanger,
|
|
87
|
+
neutral: theme.colors.foreground,
|
|
88
|
+
};
|
|
89
|
+
return byTone[tone];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface StatusPillProps {
|
|
93
|
+
theme: PluginTheme;
|
|
94
|
+
label: string;
|
|
95
|
+
tone: StatusTone;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Status text in its status token on the neutral pill shell. */
|
|
99
|
+
export function StatusPill({ theme, label, tone }: StatusPillProps) {
|
|
100
|
+
return (
|
|
101
|
+
<View
|
|
102
|
+
style={{
|
|
103
|
+
alignSelf: "flex-start",
|
|
104
|
+
paddingHorizontal: 8,
|
|
105
|
+
paddingVertical: 4,
|
|
106
|
+
borderWidth: 1,
|
|
107
|
+
borderColor: theme.colors.border,
|
|
108
|
+
borderRadius: 999,
|
|
109
|
+
backgroundColor: theme.colors.surface2,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Text style={{ color: toneColor(theme, tone), fontSize: 12, fontWeight: "400" }}>
|
|
113
|
+
{label}
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ChoiceOption<Value extends string> {
|
|
120
|
+
value: Value;
|
|
121
|
+
label: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface ChoicesProps<Value extends string> {
|
|
125
|
+
theme: PluginTheme;
|
|
126
|
+
options: readonly ChoiceOption<Value>[];
|
|
127
|
+
/** Null when nothing is selected yet. */
|
|
128
|
+
value: Value | null;
|
|
129
|
+
onChange(value: Value): void;
|
|
130
|
+
accessibilityLabel(option: ChoiceOption<Value>): string;
|
|
131
|
+
disabled?: boolean;
|
|
132
|
+
/** `tab` renders a tablist; otherwise a radio group. */
|
|
133
|
+
role?: "radio" | "tab";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Segmented choice: the selected segment is a surface fill with foreground text. */
|
|
137
|
+
export function Choices<Value extends string>({
|
|
138
|
+
theme,
|
|
139
|
+
options,
|
|
140
|
+
value,
|
|
141
|
+
onChange,
|
|
142
|
+
accessibilityLabel,
|
|
143
|
+
disabled = false,
|
|
144
|
+
role = "radio",
|
|
145
|
+
}: ChoicesProps<Value>) {
|
|
146
|
+
return (
|
|
147
|
+
<View
|
|
148
|
+
accessibilityRole={role === "tab" ? "tablist" : "radiogroup"}
|
|
149
|
+
style={{
|
|
150
|
+
flexDirection: "row",
|
|
151
|
+
flexWrap: "wrap",
|
|
152
|
+
alignSelf: role === "tab" ? "stretch" : "flex-start",
|
|
153
|
+
gap: 4,
|
|
154
|
+
padding: 4,
|
|
155
|
+
borderWidth: 1,
|
|
156
|
+
borderColor: theme.colors.border,
|
|
157
|
+
borderRadius: 12,
|
|
158
|
+
backgroundColor: theme.colors.surface1,
|
|
159
|
+
opacity: disabled ? 0.5 : 1,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{options.map((option) => (
|
|
163
|
+
<ChoiceSegment
|
|
164
|
+
key={option.value}
|
|
165
|
+
accessibilityLabel={accessibilityLabel(option)}
|
|
166
|
+
disabled={disabled}
|
|
167
|
+
label={option.label}
|
|
168
|
+
onPress={() => onChange(option.value)}
|
|
169
|
+
role={role}
|
|
170
|
+
selected={value === option.value}
|
|
171
|
+
stretch={role === "tab"}
|
|
172
|
+
theme={theme}
|
|
173
|
+
/>
|
|
174
|
+
))}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ChoiceSegmentProps {
|
|
180
|
+
theme: PluginTheme;
|
|
181
|
+
label: string;
|
|
182
|
+
accessibilityLabel: string;
|
|
183
|
+
selected: boolean;
|
|
184
|
+
disabled: boolean;
|
|
185
|
+
stretch: boolean;
|
|
186
|
+
role: "radio" | "tab";
|
|
187
|
+
onPress(): void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ChoiceSegment({
|
|
191
|
+
theme,
|
|
192
|
+
label,
|
|
193
|
+
accessibilityLabel,
|
|
194
|
+
selected,
|
|
195
|
+
disabled,
|
|
196
|
+
stretch,
|
|
197
|
+
role,
|
|
198
|
+
onPress,
|
|
199
|
+
}: ChoiceSegmentProps) {
|
|
200
|
+
const state = role === "tab" ? { selected, disabled } : { checked: selected, disabled };
|
|
201
|
+
return (
|
|
202
|
+
<Pressable
|
|
203
|
+
accessibilityLabel={accessibilityLabel}
|
|
204
|
+
accessibilityRole={role}
|
|
205
|
+
accessibilityState={state}
|
|
206
|
+
disabled={disabled}
|
|
207
|
+
onPress={onPress}
|
|
208
|
+
style={{
|
|
209
|
+
flexGrow: stretch ? 1 : 0,
|
|
210
|
+
minHeight: 32,
|
|
211
|
+
alignItems: "center",
|
|
212
|
+
justifyContent: "center",
|
|
213
|
+
paddingHorizontal: 12,
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
backgroundColor: selected ? theme.colors.surface2 : "transparent",
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<Text
|
|
219
|
+
style={{
|
|
220
|
+
color: selected ? theme.colors.foreground : theme.colors.foregroundMuted,
|
|
221
|
+
fontSize: 13,
|
|
222
|
+
fontWeight: "500",
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
{label}
|
|
226
|
+
</Text>
|
|
227
|
+
</Pressable>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface CardProps {
|
|
232
|
+
theme: PluginTheme;
|
|
233
|
+
children: ReactNode;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A bordered group of related content. */
|
|
237
|
+
export function Card({ theme, children }: CardProps) {
|
|
238
|
+
return (
|
|
239
|
+
<View
|
|
240
|
+
style={{
|
|
241
|
+
width: "100%",
|
|
242
|
+
gap: 12,
|
|
243
|
+
padding: 16,
|
|
244
|
+
borderWidth: 1,
|
|
245
|
+
borderColor: theme.colors.border,
|
|
246
|
+
borderRadius: 12,
|
|
247
|
+
backgroundColor: theme.colors.surface1,
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{children}
|
|
251
|
+
</View>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface PendingProps {
|
|
256
|
+
theme: PluginTheme;
|
|
257
|
+
label: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Inline loading: the host spinner beside a short in-flight label. */
|
|
261
|
+
export function Pending({ theme, label }: PendingProps) {
|
|
262
|
+
return (
|
|
263
|
+
<View
|
|
264
|
+
accessibilityLiveRegion="polite"
|
|
265
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
|
|
266
|
+
>
|
|
267
|
+
<View style={{ width: 16, height: 16 }}>
|
|
268
|
+
<Loading />
|
|
269
|
+
</View>
|
|
270
|
+
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{label}</Text>
|
|
271
|
+
</View>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function errorMessage(error: unknown): string {
|
|
276
|
+
return error instanceof Error ? error.message : String(error);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface ErrorTextProps {
|
|
280
|
+
theme: PluginTheme;
|
|
281
|
+
error: unknown;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** One-sentence failure under the control it belongs to. Renders nothing without an error. */
|
|
285
|
+
export function ErrorText({ theme, error }: ErrorTextProps) {
|
|
286
|
+
if (!error) return null;
|
|
287
|
+
return (
|
|
288
|
+
<Text
|
|
289
|
+
accessibilityRole="alert"
|
|
290
|
+
style={{ color: theme.colors.statusDanger, fontSize: 12, lineHeight: 16 }}
|
|
291
|
+
>
|
|
292
|
+
{errorMessage(error)}
|
|
293
|
+
</Text>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
2
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import {
|
|
4
|
+
ScrollView,
|
|
5
|
+
SettingsAction,
|
|
6
|
+
SettingsCard,
|
|
7
|
+
SettingsSection,
|
|
8
|
+
SettingsSelect,
|
|
9
|
+
} from "@getpaseo/plugin/client/ui";
|
|
10
|
+
import { useMutation } from "@tanstack/react-query";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { Text, View } from "react-native";
|
|
13
|
+
import {
|
|
14
|
+
type DoctorReport,
|
|
15
|
+
type DoctorStatusRow,
|
|
16
|
+
getContextModeDoctorReport,
|
|
17
|
+
} from "../shared/actions";
|
|
18
|
+
import { CONTEXT_MODE_PROVIDERS, type ContextModeProvider } from "../shared/providers";
|
|
19
|
+
import {
|
|
20
|
+
ActionButton,
|
|
21
|
+
Card,
|
|
22
|
+
type ChoiceOption,
|
|
23
|
+
errorMessage,
|
|
24
|
+
Pending,
|
|
25
|
+
StatusPill,
|
|
26
|
+
type StatusTone,
|
|
27
|
+
} from "./controls";
|
|
28
|
+
|
|
29
|
+
const PROVIDER_OPTIONS: readonly ChoiceOption<ContextModeProvider>[] = CONTEXT_MODE_PROVIDERS.map(
|
|
30
|
+
({ provider, label }) => ({ value: provider, label }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
interface DoctorStatusPresentation {
|
|
34
|
+
label: string;
|
|
35
|
+
tone: StatusTone;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DOCTOR_STATUS: Record<DoctorStatusRow["status"], DoctorStatusPresentation> = {
|
|
39
|
+
ok: { label: "OK", tone: "success" },
|
|
40
|
+
warning: { label: "Warning", tone: "warning" },
|
|
41
|
+
error: { label: "Error", tone: "danger" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface DiagnosticsSectionProps {
|
|
45
|
+
theme: PluginTheme;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function DiagnosticsSection({ theme }: DiagnosticsSectionProps) {
|
|
49
|
+
const loadDoctor = useRpc(getContextModeDoctorReport);
|
|
50
|
+
const [provider, setProvider] = useState<ContextModeProvider>("claude");
|
|
51
|
+
const doctor = useMutation({
|
|
52
|
+
mutationFn: (target: ContextModeProvider) => loadDoctor({ provider: target }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function changeProvider(next: ContextModeProvider) {
|
|
56
|
+
setProvider(next);
|
|
57
|
+
doctor.reset();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<SettingsSection
|
|
62
|
+
title="Diagnostics"
|
|
63
|
+
info="Doctor runs against a Paseo-owned probe store, so your project data is untouched."
|
|
64
|
+
>
|
|
65
|
+
<View style={{ gap: 16 }}>
|
|
66
|
+
<SettingsCard>
|
|
67
|
+
<SettingsSelect
|
|
68
|
+
label="Provider"
|
|
69
|
+
value={provider}
|
|
70
|
+
options={PROVIDER_OPTIONS}
|
|
71
|
+
disabled={doctor.isPending}
|
|
72
|
+
onValueChange={changeProvider}
|
|
73
|
+
/>
|
|
74
|
+
<SettingsAction
|
|
75
|
+
label="Doctor"
|
|
76
|
+
hint="Health checks for the Context Mode runtime"
|
|
77
|
+
actionLabel={doctor.isPending ? "Running..." : "Run doctor"}
|
|
78
|
+
disabled={doctor.isPending}
|
|
79
|
+
error={doctor.error ? errorMessage(doctor.error) : null}
|
|
80
|
+
onPress={() => doctor.mutate(provider)}
|
|
81
|
+
/>
|
|
82
|
+
</SettingsCard>
|
|
83
|
+
{doctor.isPending ? <Pending theme={theme} label="Running diagnostics..." /> : null}
|
|
84
|
+
{doctor.data ? <DoctorResult report={doctor.data} theme={theme} /> : null}
|
|
85
|
+
</View>
|
|
86
|
+
</SettingsSection>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface DoctorResultProps {
|
|
91
|
+
report: DoctorReport;
|
|
92
|
+
theme: PluginTheme;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function DoctorResult({ report, theme }: DoctorResultProps) {
|
|
96
|
+
const [showRaw, setShowRaw] = useState(false);
|
|
97
|
+
const hasRows = report.rows.length > 0;
|
|
98
|
+
const hasRawOutput = report.rawOutput.trim().length > 0;
|
|
99
|
+
const rawVisible = !hasRows || showRaw;
|
|
100
|
+
const mutedStyle = { color: theme.colors.foregroundMuted, fontSize: 12 };
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Card theme={theme}>
|
|
104
|
+
<View accessibilityLiveRegion="polite" style={{ gap: 12 }}>
|
|
105
|
+
<Text style={mutedStyle}>Checked as {report.platform}</Text>
|
|
106
|
+
{report.rows.map((row) => (
|
|
107
|
+
<DoctorRow key={`${row.status}:${row.check}:${row.detail}`} row={row} theme={theme} />
|
|
108
|
+
))}
|
|
109
|
+
{hasRows && hasRawOutput ? (
|
|
110
|
+
<ActionButton
|
|
111
|
+
theme={theme}
|
|
112
|
+
variant="outline"
|
|
113
|
+
expanded={showRaw}
|
|
114
|
+
label={showRaw ? "Hide raw diagnostics" : "Show raw diagnostics"}
|
|
115
|
+
onPress={() => setShowRaw((visible) => !visible)}
|
|
116
|
+
/>
|
|
117
|
+
) : null}
|
|
118
|
+
{rawVisible ? <RawOutput output={report.rawOutput} theme={theme} /> : null}
|
|
119
|
+
</View>
|
|
120
|
+
</Card>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface DoctorRowProps {
|
|
125
|
+
row: DoctorStatusRow;
|
|
126
|
+
theme: PluginTheme;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function DoctorRow({ row, theme }: DoctorRowProps) {
|
|
130
|
+
const status = DOCTOR_STATUS[row.status];
|
|
131
|
+
const detailStyle = { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 16 };
|
|
132
|
+
return (
|
|
133
|
+
<View accessibilityLabel={`${row.check}: ${status.label}. ${row.detail}`} style={{ gap: 4 }}>
|
|
134
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
|
|
135
|
+
<Text style={{ flexShrink: 1, color: theme.colors.foreground }}>{row.check}</Text>
|
|
136
|
+
<StatusPill theme={theme} label={status.label} tone={status.tone} />
|
|
137
|
+
</View>
|
|
138
|
+
{row.detail ? (
|
|
139
|
+
<Text selectable style={detailStyle}>
|
|
140
|
+
{row.detail}
|
|
141
|
+
</Text>
|
|
142
|
+
) : null}
|
|
143
|
+
{row.fix ? (
|
|
144
|
+
<Text selectable style={detailStyle}>
|
|
145
|
+
Fix: {row.fix}
|
|
146
|
+
</Text>
|
|
147
|
+
) : null}
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface RawOutputProps {
|
|
153
|
+
output: string;
|
|
154
|
+
theme: PluginTheme;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function RawOutput({ output, theme }: RawOutputProps) {
|
|
158
|
+
return (
|
|
159
|
+
<View style={{ gap: 4 }}>
|
|
160
|
+
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, fontWeight: "500" }}>
|
|
161
|
+
Raw diagnostic output
|
|
162
|
+
</Text>
|
|
163
|
+
<ScrollView nestedScrollEnabled style={{ maxHeight: 220 }}>
|
|
164
|
+
<Text
|
|
165
|
+
selectable
|
|
166
|
+
style={{
|
|
167
|
+
color: theme.colors.foreground,
|
|
168
|
+
fontFamily: "monospace",
|
|
169
|
+
fontSize: 12,
|
|
170
|
+
lineHeight: 16,
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{output || "No diagnostic text"}
|
|
174
|
+
</Text>
|
|
175
|
+
</ScrollView>
|
|
176
|
+
</View>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -2,11 +2,20 @@ import { type PluginSidebarItemProps, useRpc, useSettings } from "@getpaseo/plug
|
|
|
2
2
|
import { SidebarRow } from "@getpaseo/plugin/client/ui";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { Text } from "react-native";
|
|
5
|
-
import {
|
|
5
|
+
import { getContextModeStatus } from "../shared/contracts";
|
|
6
|
+
import { contextModeSettings } from "../shared/settings";
|
|
6
7
|
|
|
7
8
|
// The footer stays mounted all session; poll slowly because each status miss spawns the binary.
|
|
8
9
|
const FOOTER_REFRESH_MS = 60_000;
|
|
9
10
|
|
|
11
|
+
type FooterHealth = "failing" | "healthy" | "checking";
|
|
12
|
+
|
|
13
|
+
const HEALTH_LABELS: Record<FooterHealth, string> = {
|
|
14
|
+
failing: "Failing",
|
|
15
|
+
healthy: "Healthy",
|
|
16
|
+
checking: "Checking",
|
|
17
|
+
};
|
|
18
|
+
|
|
10
19
|
export function ContextModeFooter({
|
|
11
20
|
theme,
|
|
12
21
|
host,
|
|
@@ -22,13 +31,17 @@ export function ContextModeFooter({
|
|
|
22
31
|
staleTime: FOOTER_REFRESH_MS,
|
|
23
32
|
enabled: settings.status === "ready",
|
|
24
33
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
34
|
+
const settingsFailed = settings.status === "error" || settings.status === "invalid";
|
|
35
|
+
const statusFailed = Boolean(status.error) || status.data?.state === "unavailable";
|
|
36
|
+
const statusReady = settings.status === "ready" && status.data?.state === "ready";
|
|
37
|
+
let health: FooterHealth = "checking";
|
|
38
|
+
if (settingsFailed || statusFailed) health = "failing";
|
|
39
|
+
else if (statusReady) health = "healthy";
|
|
40
|
+
const healthColors: Record<FooterHealth, string> = {
|
|
41
|
+
failing: theme.colors.statusDanger,
|
|
42
|
+
healthy: theme.colors.statusSuccess,
|
|
43
|
+
checking: theme.colors.foregroundMuted,
|
|
44
|
+
};
|
|
32
45
|
return (
|
|
33
46
|
<SidebarRow
|
|
34
47
|
icon="Gauge"
|
|
@@ -36,18 +49,7 @@ export function ContextModeFooter({
|
|
|
36
49
|
active={currentScreen?.screenId === "context-mode"}
|
|
37
50
|
onPress={() => openScreen({ screenId: "context-mode" })}
|
|
38
51
|
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>
|
|
52
|
+
<Text style={{ color: healthColors[health], fontSize: 12 }}>{HEALTH_LABELS[health]}</Text>
|
|
51
53
|
}
|
|
52
54
|
/>
|
|
53
55
|
);
|