@omercnet/paseo-context-mode 0.0.0

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