@omercnet/paseo-context-mode 0.0.0-next.127.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,621 @@
1
+ import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ ActivityIndicator,
6
+ Pressable,
7
+ ScrollView,
8
+ Text,
9
+ type TextStyle,
10
+ View,
11
+ type ViewStyle,
12
+ } from "react-native";
13
+ import {
14
+ CONTEXT_MODE_TARGET_VERSION,
15
+ type ContextModeAction,
16
+ type DoctorReport,
17
+ getContextModeDoctorReport,
18
+ getContextModeInstallAction,
19
+ getContextModeUpgradeAction,
20
+ } from "../shared/actions";
21
+ import type { ContextModeProvider } from "../shared/knowledge";
22
+
23
+ const PROVIDERS = [
24
+ { value: "claude", label: "Claude" },
25
+ { value: "codex", label: "Codex" },
26
+ { value: "cursor", label: "Cursor" },
27
+ { value: "copilot", label: "Copilot" },
28
+ { value: "opencode", label: "OpenCode" },
29
+ { value: "pi", label: "Pi" },
30
+ { value: "omp", label: "OMP" },
31
+ { value: "omp-plugin", label: "OMP plugin" },
32
+ ] as const satisfies ReadonlyArray<{ value: ContextModeProvider; label: string }>;
33
+
34
+ export type ActionsSectionProps = Pick<PluginSurfaceProps, "theme" | "layout">;
35
+
36
+ interface ActionsStyles {
37
+ section: ViewStyle;
38
+ sectionHeader: ViewStyle;
39
+ sectionTitle: TextStyle;
40
+ card: ViewStyle;
41
+ cardHeader: ViewStyle;
42
+ cardTitle: TextStyle;
43
+ muted: TextStyle;
44
+ choiceRow: ViewStyle;
45
+ choice: ViewStyle;
46
+ choiceSelected: ViewStyle;
47
+ choiceText: TextStyle;
48
+ choiceTextSelected: TextStyle;
49
+ scopeNote: TextStyle;
50
+ actions: ViewStyle;
51
+ button: ViewStyle;
52
+ primaryButton: ViewStyle;
53
+ disabled: ViewStyle;
54
+ buttonText: TextStyle;
55
+ primaryButtonText: TextStyle;
56
+ pendingRow: ViewStyle;
57
+ activity: { color: string };
58
+ error: TextStyle;
59
+ doctorRows: ViewStyle;
60
+ doctorRow: ViewStyle;
61
+ doctorMarker: ViewStyle;
62
+ doctorMarkerOk: ViewStyle;
63
+ doctorMarkerWarning: ViewStyle;
64
+ doctorMarkerError: ViewStyle;
65
+ doctorCopy: ViewStyle;
66
+ doctorTopline: ViewStyle;
67
+ doctorCheck: TextStyle;
68
+ doctorStatus: TextStyle;
69
+ doctorStatusOk: TextStyle;
70
+ doctorStatusWarning: TextStyle;
71
+ doctorStatusError: TextStyle;
72
+ doctorDetail: TextStyle;
73
+ rawFrame: ViewStyle;
74
+ rawOutput: TextStyle;
75
+ commandCard: ViewStyle;
76
+ commandTitle: TextStyle;
77
+ property: ViewStyle;
78
+ propertyLabel: TextStyle;
79
+ propertyValue: TextStyle;
80
+ argumentFrame: ViewStyle;
81
+ argumentRow: ViewStyle;
82
+ argumentIndex: TextStyle;
83
+ argumentValue: TextStyle;
84
+ metaRow: ViewStyle;
85
+ badge: ViewStyle;
86
+ badgeText: TextStyle;
87
+ warningBadge: ViewStyle;
88
+ warningBadgeText: TextStyle;
89
+ }
90
+
91
+ function ActionButton({
92
+ label,
93
+ onPress,
94
+ disabled = false,
95
+ primary = false,
96
+ styles,
97
+ }: {
98
+ label: string;
99
+ onPress(): void;
100
+ disabled?: boolean;
101
+ primary?: boolean;
102
+ styles: ActionsStyles;
103
+ }) {
104
+ return (
105
+ <Pressable
106
+ accessibilityLabel={label}
107
+ accessibilityRole="button"
108
+ accessibilityState={{ disabled }}
109
+ disabled={disabled}
110
+ onPress={onPress}
111
+ style={[
112
+ styles.button,
113
+ primary ? styles.primaryButton : null,
114
+ disabled ? styles.disabled : null,
115
+ ]}
116
+ >
117
+ <Text style={[styles.buttonText, primary ? styles.primaryButtonText : null]}>{label}</Text>
118
+ </Pressable>
119
+ );
120
+ }
121
+
122
+ function DoctorResult({
123
+ report,
124
+ pending,
125
+ error,
126
+ styles,
127
+ }: {
128
+ report: DoctorReport | undefined;
129
+ pending: boolean;
130
+ error: unknown;
131
+ styles: ActionsStyles;
132
+ }) {
133
+ const [showRaw, setShowRaw] = useState(false);
134
+ if (pending) {
135
+ return (
136
+ <View accessibilityLiveRegion="polite" style={styles.pendingRow}>
137
+ <ActivityIndicator size="small" color={styles.activity.color} />
138
+ <Text style={styles.muted}>Running diagnostics…</Text>
139
+ </View>
140
+ );
141
+ }
142
+ if (error) {
143
+ return (
144
+ <Text accessibilityRole="alert" style={styles.error}>
145
+ {error instanceof Error ? error.message : String(error)}
146
+ </Text>
147
+ );
148
+ }
149
+ if (!report) return null;
150
+
151
+ const hasRows = report.rows.length > 0;
152
+ const rawVisible = !hasRows || showRaw;
153
+ return (
154
+ <View accessibilityLiveRegion="polite" style={styles.doctorRows}>
155
+ <View style={styles.metaRow}>
156
+ <View style={styles.badge}>
157
+ <Text style={styles.badgeText}>{report.platform}</Text>
158
+ </View>
159
+ {hasRows && report.rawOutput.trim() ? (
160
+ <ActionButton
161
+ label={showRaw ? "Hide raw diagnostics" : "Show raw diagnostics"}
162
+ onPress={() => setShowRaw((visible) => !visible)}
163
+ styles={styles}
164
+ />
165
+ ) : null}
166
+ </View>
167
+ {report.rows.map((row) => {
168
+ const ok = row.status === "ok";
169
+ const warning = row.status === "warning";
170
+ const statusLabel = ok ? "OK" : warning ? "Warning" : "Error";
171
+ return (
172
+ <View
173
+ key={`${row.status}:${row.check}:${row.detail}`}
174
+ accessibilityLabel={`${row.check}: ${statusLabel}. ${row.detail}`}
175
+ style={styles.doctorRow}
176
+ >
177
+ <View
178
+ style={[
179
+ styles.doctorMarker,
180
+ ok
181
+ ? styles.doctorMarkerOk
182
+ : warning
183
+ ? styles.doctorMarkerWarning
184
+ : styles.doctorMarkerError,
185
+ ]}
186
+ />
187
+ <View style={styles.doctorCopy}>
188
+ <View style={styles.doctorTopline}>
189
+ <Text style={styles.doctorCheck}>{row.check}</Text>
190
+ <Text
191
+ style={[
192
+ styles.doctorStatus,
193
+ ok
194
+ ? styles.doctorStatusOk
195
+ : warning
196
+ ? styles.doctorStatusWarning
197
+ : styles.doctorStatusError,
198
+ ]}
199
+ >
200
+ {statusLabel}
201
+ </Text>
202
+ </View>
203
+ {row.detail ? (
204
+ <Text selectable style={styles.doctorDetail}>
205
+ {row.detail}
206
+ </Text>
207
+ ) : null}
208
+ </View>
209
+ </View>
210
+ );
211
+ })}
212
+ {rawVisible ? (
213
+ <View style={styles.rawFrame}>
214
+ <Text style={styles.propertyLabel}>Raw diagnostic output</Text>
215
+ <ScrollView nestedScrollEnabled style={styles.argumentFrame}>
216
+ <Text selectable style={styles.rawOutput}>
217
+ {report.rawOutput || "Context Mode returned no diagnostic text."}
218
+ </Text>
219
+ </ScrollView>
220
+ </View>
221
+ ) : null}
222
+ </View>
223
+ );
224
+ }
225
+
226
+ function CommandResult({
227
+ title,
228
+ action,
229
+ pending,
230
+ error,
231
+ styles,
232
+ }: {
233
+ title: string;
234
+ action: ContextModeAction | undefined;
235
+ pending: boolean;
236
+ error: unknown;
237
+ styles: ActionsStyles;
238
+ }) {
239
+ if (pending) {
240
+ return (
241
+ <View accessibilityLiveRegion="polite" style={styles.pendingRow}>
242
+ <ActivityIndicator size="small" color={styles.activity.color} />
243
+ <Text style={styles.muted}>Preparing {title.toLowerCase()}…</Text>
244
+ </View>
245
+ );
246
+ }
247
+ if (error) {
248
+ return (
249
+ <Text accessibilityRole="alert" style={styles.error}>
250
+ {error instanceof Error ? error.message : String(error)}
251
+ </Text>
252
+ );
253
+ }
254
+ if (!action) return null;
255
+
256
+ const seenArguments = new Map<string, number>();
257
+ const argumentsWithKeys = action.args.map((argument, index) => {
258
+ const occurrence = seenArguments.get(argument) ?? 0;
259
+ seenArguments.set(argument, occurrence + 1);
260
+ return { argument, position: index + 1, key: `${argument}:${occurrence}` };
261
+ });
262
+
263
+ return (
264
+ <View accessibilityLiveRegion="polite" style={styles.commandCard}>
265
+ <View style={styles.cardHeader}>
266
+ <Text style={styles.commandTitle}>{title}</Text>
267
+ <Text style={styles.muted}>
268
+ Review this executable and argument list before running it outside Paseo.
269
+ </Text>
270
+ </View>
271
+ <View style={styles.property}>
272
+ <Text style={styles.propertyLabel}>Program</Text>
273
+ <Text selectable style={styles.propertyValue}>
274
+ {action.program}
275
+ </Text>
276
+ </View>
277
+ <View style={styles.property}>
278
+ <Text style={styles.propertyLabel}>Arguments ({action.args.length})</Text>
279
+ <ScrollView nestedScrollEnabled style={styles.argumentFrame}>
280
+ {action.args.length ? (
281
+ argumentsWithKeys.map(({ argument, key, position }) => (
282
+ <View key={key} style={styles.argumentRow}>
283
+ <Text style={styles.argumentIndex}>{position}</Text>
284
+ <Text selectable style={styles.argumentValue}>
285
+ {argument}
286
+ </Text>
287
+ </View>
288
+ ))
289
+ ) : (
290
+ <Text style={styles.muted}>No arguments.</Text>
291
+ )}
292
+ </ScrollView>
293
+ </View>
294
+ <View style={styles.metaRow}>
295
+ <View style={styles.badge}>
296
+ <Text style={styles.badgeText}>{action.platform}</Text>
297
+ </View>
298
+ <View style={styles.badge}>
299
+ <Text style={styles.badgeText}>
300
+ {action.currentVersion ? `${action.currentVersion} → ` : "Install "}
301
+ {action.targetVersion}
302
+ </Text>
303
+ </View>
304
+ {action.requiresRestart ? (
305
+ <View style={styles.warningBadge}>
306
+ <Text style={styles.warningBadgeText}>Restart required</Text>
307
+ </View>
308
+ ) : null}
309
+ </View>
310
+ </View>
311
+ );
312
+ }
313
+
314
+ export function ActionsSection({ theme, layout }: ActionsSectionProps) {
315
+ const styles = useMemo(() => createStyles(theme, layout.compact), [layout.compact, theme]);
316
+ const loadDoctor = useRpc(getContextModeDoctorReport);
317
+ const loadInstall = useRpc(getContextModeInstallAction);
318
+ const loadUpgrade = useRpc(getContextModeUpgradeAction);
319
+ const [provider, setProvider] = useState<ContextModeProvider | null>("claude");
320
+ const doctor = useMutation({
321
+ mutationFn: () => {
322
+ if (!provider) throw new Error("Select a provider before running diagnostics.");
323
+ return loadDoctor({ provider });
324
+ },
325
+ });
326
+ const install = useMutation({
327
+ mutationFn: () => {
328
+ if (!provider) throw new Error("Select a provider before preparing an install command.");
329
+ return loadInstall({ provider });
330
+ },
331
+ });
332
+ const upgrade = useMutation({
333
+ mutationFn: () => {
334
+ if (!provider) throw new Error("Select a provider before preparing an upgrade command.");
335
+ return loadUpgrade({ provider });
336
+ },
337
+ });
338
+
339
+ const changeProvider = (next: ContextModeProvider) => {
340
+ setProvider(next);
341
+ doctor.reset();
342
+ install.reset();
343
+ upgrade.reset();
344
+ };
345
+ const busy = doctor.isPending || install.isPending || upgrade.isPending;
346
+
347
+ return (
348
+ <View style={styles.section}>
349
+ <View style={styles.sectionHeader}>
350
+ <Text accessibilityRole="header" style={styles.sectionTitle}>
351
+ Diagnostics and setup
352
+ </Text>
353
+ <Text style={styles.muted}>Health checks, installation, and upgrades.</Text>
354
+ </View>
355
+
356
+ <View style={styles.card}>
357
+ <Text style={styles.cardTitle}>Target provider</Text>
358
+ <View accessibilityRole="radiogroup" style={styles.choiceRow}>
359
+ {PROVIDERS.map((option) => {
360
+ const selected = provider === option.value;
361
+ return (
362
+ <Pressable
363
+ key={option.value}
364
+ accessibilityLabel={`${option.label} action target`}
365
+ accessibilityRole="radio"
366
+ accessibilityState={{ checked: selected, disabled: busy }}
367
+ disabled={busy}
368
+ onPress={() => changeProvider(option.value)}
369
+ style={[
370
+ styles.choice,
371
+ selected ? styles.choiceSelected : null,
372
+ busy ? styles.disabled : null,
373
+ ]}
374
+ >
375
+ <Text style={[styles.choiceText, selected ? styles.choiceTextSelected : null]}>
376
+ {option.label}
377
+ </Text>
378
+ </Pressable>
379
+ );
380
+ })}
381
+ </View>
382
+ <Text accessibilityLiveRegion="polite" style={styles.scopeNote}>
383
+ {provider
384
+ ? `Diagnostics and commands will target ${PROVIDERS.find((item) => item.value === provider)?.label}.`
385
+ : "Select a provider before requesting a diagnostic or command."}
386
+ </Text>
387
+ </View>
388
+
389
+ <View style={styles.card}>
390
+ <View style={styles.cardHeader}>
391
+ <Text accessibilityRole="header" style={styles.cardTitle}>
392
+ Doctor
393
+ </Text>
394
+ <Text style={styles.muted}>
395
+ Run read-only checks. Structured rows are shown first; the original diagnostic text
396
+ remains available as a fallback.
397
+ </Text>
398
+ </View>
399
+ <View style={styles.actions}>
400
+ <ActionButton
401
+ disabled={!provider || busy}
402
+ label={doctor.isPending ? "Running doctor…" : "Run doctor"}
403
+ onPress={() => doctor.mutate()}
404
+ primary
405
+ styles={styles}
406
+ />
407
+ </View>
408
+ <DoctorResult
409
+ error={doctor.error}
410
+ pending={doctor.isPending}
411
+ report={doctor.data}
412
+ styles={styles}
413
+ />
414
+ </View>
415
+
416
+ <View style={styles.card}>
417
+ <View style={styles.cardHeader}>
418
+ <Text accessibilityRole="header" style={styles.cardTitle}>
419
+ Install or upgrade
420
+ </Text>
421
+ <Text style={styles.muted}>
422
+ Install requests pin Context Mode {CONTEXT_MODE_TARGET_VERSION}; upgrades target the
423
+ latest release. Paseo shows the exact command; run it yourself.
424
+ </Text>
425
+ </View>
426
+ <View style={styles.actions}>
427
+ <ActionButton
428
+ disabled={!provider || busy}
429
+ label={install.isPending ? "Preparing install…" : "Prepare install"}
430
+ onPress={() => install.mutate()}
431
+ styles={styles}
432
+ />
433
+ <ActionButton
434
+ disabled={!provider || busy}
435
+ label={upgrade.isPending ? "Preparing upgrade…" : "Prepare upgrade"}
436
+ onPress={() => upgrade.mutate()}
437
+ styles={styles}
438
+ />
439
+ </View>
440
+ <CommandResult
441
+ action={install.data}
442
+ error={install.error}
443
+ pending={install.isPending}
444
+ styles={styles}
445
+ title="Install command"
446
+ />
447
+ <CommandResult
448
+ action={upgrade.data}
449
+ error={upgrade.error}
450
+ pending={upgrade.isPending}
451
+ styles={styles}
452
+ title="Upgrade command"
453
+ />
454
+ </View>
455
+ </View>
456
+ );
457
+ }
458
+
459
+ function createStyles(theme: PluginSurfaceProps["theme"], compact: boolean): ActionsStyles {
460
+ return {
461
+ section: { width: "100%", gap: compact ? 12 : 16 },
462
+ sectionHeader: { gap: 4 },
463
+ sectionTitle: {
464
+ color: theme.colors.foreground,
465
+ fontSize: compact ? 18 : 20,
466
+ fontWeight: "700",
467
+ },
468
+ card: {
469
+ width: "100%",
470
+ gap: 12,
471
+ padding: compact ? 12 : 16,
472
+ borderWidth: 1,
473
+ borderColor: theme.colors.border,
474
+ borderRadius: 12,
475
+ backgroundColor: theme.colors.surface1,
476
+ },
477
+ cardHeader: { gap: 3 },
478
+ cardTitle: { color: theme.colors.foreground, fontSize: 15, fontWeight: "700" },
479
+ muted: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
480
+ choiceRow: { flexDirection: "row", flexWrap: "wrap", gap: 7 },
481
+ choice: {
482
+ minHeight: 36,
483
+ justifyContent: "center",
484
+ paddingHorizontal: 11,
485
+ paddingVertical: 7,
486
+ borderWidth: 1,
487
+ borderColor: theme.colors.border,
488
+ borderRadius: 999,
489
+ backgroundColor: theme.colors.surface0,
490
+ },
491
+ choiceSelected: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
492
+ choiceText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
493
+ choiceTextSelected: { color: theme.colors.accentForeground },
494
+ scopeNote: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
495
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
496
+ button: {
497
+ minHeight: 38,
498
+ alignItems: "center",
499
+ justifyContent: "center",
500
+ paddingHorizontal: 13,
501
+ paddingVertical: 8,
502
+ borderWidth: 1,
503
+ borderColor: theme.colors.border,
504
+ borderRadius: 8,
505
+ backgroundColor: theme.colors.surface2,
506
+ },
507
+ primaryButton: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
508
+ disabled: { opacity: 0.5 },
509
+ buttonText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" },
510
+ primaryButtonText: { color: theme.colors.accentForeground },
511
+ pendingRow: { flexDirection: "row", alignItems: "center", gap: 8 },
512
+ activity: { color: theme.colors.accent },
513
+ error: { color: theme.colors.statusDanger, fontSize: 12, lineHeight: 17 },
514
+ doctorRows: { gap: 8 },
515
+ doctorRow: {
516
+ flexDirection: "row",
517
+ gap: 9,
518
+ padding: 10,
519
+ borderWidth: 1,
520
+ borderColor: theme.colors.border,
521
+ borderRadius: 8,
522
+ backgroundColor: theme.colors.surface0,
523
+ },
524
+ doctorMarker: { width: 4, minHeight: 32, borderRadius: 999 },
525
+ doctorMarkerOk: { backgroundColor: theme.colors.statusSuccess },
526
+ doctorMarkerWarning: { backgroundColor: theme.colors.statusWarning },
527
+ doctorMarkerError: { backgroundColor: theme.colors.statusDanger },
528
+ doctorCopy: { flex: 1, minWidth: 0, gap: 3 },
529
+ doctorTopline: {
530
+ flexDirection: compact ? "column" : "row",
531
+ alignItems: compact ? "flex-start" : "center",
532
+ gap: 6,
533
+ },
534
+ doctorCheck: {
535
+ flex: compact ? undefined : 1,
536
+ color: theme.colors.foreground,
537
+ fontWeight: "600",
538
+ },
539
+ doctorStatus: { fontSize: 11, fontWeight: "700" },
540
+ doctorStatusOk: { color: theme.colors.statusSuccess },
541
+ doctorStatusWarning: { color: theme.colors.statusWarning },
542
+ doctorStatusError: { color: theme.colors.statusDanger },
543
+ doctorDetail: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
544
+ rawFrame: {
545
+ gap: 6,
546
+ padding: 10,
547
+ borderWidth: 1,
548
+ borderColor: theme.colors.border,
549
+ borderRadius: 8,
550
+ backgroundColor: theme.colors.surface0,
551
+ },
552
+ rawOutput: {
553
+ color: theme.colors.foreground,
554
+ fontFamily: "monospace",
555
+ fontSize: compact ? 11 : 12,
556
+ lineHeight: compact ? 16 : 18,
557
+ },
558
+ commandCard: {
559
+ gap: 10,
560
+ padding: 11,
561
+ borderWidth: 1,
562
+ borderColor: theme.colors.border,
563
+ borderRadius: 8,
564
+ backgroundColor: theme.colors.surface0,
565
+ },
566
+ commandTitle: { color: theme.colors.foreground, fontSize: 14, fontWeight: "700" },
567
+ property: { gap: 4 },
568
+ propertyLabel: {
569
+ color: theme.colors.foregroundMuted,
570
+ fontSize: 11,
571
+ fontWeight: "600",
572
+ },
573
+ propertyValue: {
574
+ color: theme.colors.foreground,
575
+ fontFamily: "monospace",
576
+ fontSize: compact ? 11 : 12,
577
+ lineHeight: compact ? 16 : 18,
578
+ },
579
+ argumentFrame: { maxHeight: compact ? 160 : 220 },
580
+ argumentRow: {
581
+ flexDirection: "row",
582
+ alignItems: "flex-start",
583
+ gap: 8,
584
+ paddingVertical: 4,
585
+ borderBottomWidth: 1,
586
+ borderBottomColor: theme.colors.border,
587
+ },
588
+ argumentIndex: {
589
+ width: 22,
590
+ color: theme.colors.foregroundMuted,
591
+ fontFamily: "monospace",
592
+ fontSize: 11,
593
+ },
594
+ argumentValue: {
595
+ flex: 1,
596
+ color: theme.colors.foreground,
597
+ fontFamily: "monospace",
598
+ fontSize: compact ? 11 : 12,
599
+ lineHeight: compact ? 16 : 18,
600
+ },
601
+ metaRow: { flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 7 },
602
+ badge: {
603
+ paddingHorizontal: 8,
604
+ paddingVertical: 4,
605
+ borderWidth: 1,
606
+ borderColor: theme.colors.border,
607
+ borderRadius: 999,
608
+ backgroundColor: theme.colors.surface2,
609
+ },
610
+ badgeText: { color: theme.colors.foreground, fontSize: 11, fontWeight: "600" },
611
+ warningBadge: {
612
+ paddingHorizontal: 8,
613
+ paddingVertical: 4,
614
+ borderWidth: 1,
615
+ borderColor: theme.colors.statusWarning,
616
+ borderRadius: 999,
617
+ backgroundColor: theme.colors.surface1,
618
+ },
619
+ warningBadgeText: { color: theme.colors.statusWarning, fontSize: 11, fontWeight: "600" },
620
+ };
621
+ }