@omercnet/paseo-gas-city 0.1.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,1505 @@
1
+ import type { PluginHostProps } from "@getpaseo/plugin/client";
2
+ import { useRpc } from "@getpaseo/plugin/client";
3
+ import { Icon, Modal, ScrollView, TextInput, useToast } from "@getpaseo/plugin/client/react-native";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { type ReactNode, useEffect, useMemo, useState } from "react";
6
+ import {
7
+ ActivityIndicator,
8
+ Pressable,
9
+ SectionList,
10
+ StyleSheet,
11
+ Text,
12
+ type TextStyle,
13
+ View,
14
+ type ViewStyle,
15
+ } from "react-native";
16
+ import {
17
+ type AttentionItem,
18
+ type DispatchRequest,
19
+ dispatchWork,
20
+ type GasCityConvoy,
21
+ type GasCityEvent,
22
+ type GasCitySession,
23
+ type GasCitySettings,
24
+ type GasCityWorkItem,
25
+ getCityRigSnapshot,
26
+ listAttention,
27
+ listConvoys,
28
+ listEvents,
29
+ listSessions,
30
+ listWork,
31
+ performSessionAction,
32
+ type SessionActionRequest,
33
+ toGasCityRpcSettings,
34
+ } from "../shared";
35
+ import type { SlingIntent } from "./dispatch-intent";
36
+ import {
37
+ buildDashboardSections,
38
+ cityQueryRoot,
39
+ convoyProgress,
40
+ type DashboardRow,
41
+ type DashboardSection,
42
+ presentSection,
43
+ refreshPresentation,
44
+ type SessionActionName,
45
+ sessionAccessibilityLabel,
46
+ sessionActionsFor,
47
+ } from "./view-model";
48
+
49
+ interface CityOperationsProps extends PluginHostProps {
50
+ cityName: string;
51
+ rigName: string | null;
52
+ settings: GasCitySettings;
53
+ slingIntent?: SlingIntent | null;
54
+ onDismissSlingIntent?: (id: number) => void;
55
+ }
56
+
57
+ type SessionAction = SessionActionName | "respond";
58
+ type InteractionResponse = "allow" | "deny" | "answer";
59
+ type SessionActionDialog = {
60
+ sessionId: string;
61
+ title: string;
62
+ actions: readonly SessionAction[];
63
+ requestId: string | null;
64
+ };
65
+ type Styles = Record<string, TextStyle | ViewStyle>;
66
+
67
+ const SESSION_ACTION_LABELS: Record<SessionAction, string> = {
68
+ wake: "Wake",
69
+ message: "Message",
70
+ submit: "Submit",
71
+ stop: "Stop",
72
+ suspend: "Suspend",
73
+ close: "Close",
74
+ kill: "Kill",
75
+ respond: "Respond",
76
+ };
77
+
78
+ function errorMessage(error: unknown): string {
79
+ return error instanceof Error ? error.message : "An unexpected error occurred.";
80
+ }
81
+
82
+ function relativeTime(value: string | null): string {
83
+ if (!value) return "never";
84
+ const ageMs = Date.now() - Date.parse(value);
85
+ if (!Number.isFinite(ageMs) || ageMs < 0) return value;
86
+ const minutes = Math.floor(ageMs / 60_000);
87
+ if (minutes < 1) return "now";
88
+ if (minutes < 60) return `${minutes}m ago`;
89
+ const hours = Math.floor(minutes / 60);
90
+ if (hours < 24) return `${hours}h ago`;
91
+ return `${Math.floor(hours / 24)}d ago`;
92
+ }
93
+
94
+ function statusColor(status: string, colors: PluginHostProps["theme"]["colors"]): string {
95
+ const normalized = status.toLowerCase();
96
+ if (/error|failed|critical|kill|quarantined/.test(normalized)) return colors.statusDanger;
97
+ if (/warn|blocked|pending|suspend|stale/.test(normalized)) return colors.statusWarning;
98
+ if (/running|active|ready|available|complete|success/.test(normalized)) {
99
+ return colors.statusSuccess;
100
+ }
101
+ return colors.foregroundMuted;
102
+ }
103
+
104
+ function rowSignalColor(status: string, colors: PluginHostProps["theme"]["colors"]): string {
105
+ const normalized = status.toLowerCase();
106
+ if (/error|failed|critical|kill|quarantined/.test(normalized)) return colors.statusDanger;
107
+ if (/warn|blocked|pending|suspend|stale/.test(normalized)) return colors.statusWarning;
108
+ return colors.foregroundMuted;
109
+ }
110
+
111
+ function StateCard({
112
+ body,
113
+ loading,
114
+ onRetry,
115
+ styles,
116
+ theme,
117
+ title,
118
+ }: {
119
+ body: string;
120
+ loading?: boolean;
121
+ onRetry?: () => void;
122
+ styles: Styles;
123
+ theme: PluginHostProps["theme"];
124
+ title: string;
125
+ }) {
126
+ return (
127
+ <View style={styles.stateCard}>
128
+ {loading ? (
129
+ <ActivityIndicator color={theme.colors.accent} />
130
+ ) : (
131
+ <Icon name="AlertTriangle" size={24} color={theme.colors.foregroundMuted} />
132
+ )}
133
+ <Text accessibilityRole="header" style={styles.stateTitle}>
134
+ {title}
135
+ </Text>
136
+ <Text style={styles.stateBody}>{body}</Text>
137
+ {onRetry ? (
138
+ <Pressable
139
+ accessibilityRole="button"
140
+ accessibilityLabel="Retry loading Gas City data"
141
+ onPress={onRetry}
142
+ style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressed]}
143
+ >
144
+ <Text style={styles.secondaryButtonText}>Retry</Text>
145
+ </Pressable>
146
+ ) : null}
147
+ </View>
148
+ );
149
+ }
150
+
151
+ function Stat({ label, value, styles }: { label: string; value: string | number; styles: Styles }) {
152
+ return (
153
+ <View style={styles.stat}>
154
+ <Text style={styles.statValue}>{value}</Text>
155
+ <Text style={styles.statLabel}>{label}</Text>
156
+ </View>
157
+ );
158
+ }
159
+
160
+ function activeSessionSummary(items: readonly GasCitySession[] | undefined): string | null {
161
+ if (!items) return null;
162
+ const active = items.filter((item) => item.running);
163
+ if (active.length === 0) return "No sessions active.";
164
+ const names = active
165
+ .slice(0, 3)
166
+ .map((item) => item.title)
167
+ .join(", ");
168
+ const overflow = active.length > 3 ? ` +${active.length - 3}` : "";
169
+ return `${active.length} ${active.length === 1 ? "session" : "sessions"} active · ${names}${overflow}`;
170
+ }
171
+
172
+ function Diagnostics({
173
+ diagnostics,
174
+ styles,
175
+ theme,
176
+ }: {
177
+ diagnostics: readonly { code: string; message: string; retryable: boolean }[];
178
+ styles: Styles;
179
+ theme: PluginHostProps["theme"];
180
+ }) {
181
+ if (diagnostics.length === 0) return null;
182
+ return (
183
+ <View accessibilityRole="alert" style={styles.diagnostics}>
184
+ <View style={styles.diagnosticsTitleRow}>
185
+ <Icon name="TriangleAlert" size={15} color={theme.colors.statusWarning} />
186
+ <Text style={styles.diagnosticsTitle}>Diagnostics</Text>
187
+ </View>
188
+ {diagnostics.map((diagnostic) => (
189
+ <Text key={`${diagnostic.code}:${diagnostic.message}`} style={styles.diagnosticText}>
190
+ {diagnostic.code}: {diagnostic.message}
191
+ {diagnostic.retryable ? " · retryable" : ""}
192
+ </Text>
193
+ ))}
194
+ </View>
195
+ );
196
+ }
197
+
198
+ function AttentionRow({
199
+ item,
200
+ onRespond,
201
+ styles,
202
+ theme,
203
+ }: {
204
+ item: AttentionItem;
205
+ onRespond?: () => void;
206
+ styles: Styles;
207
+ theme: PluginHostProps["theme"];
208
+ }) {
209
+ return (
210
+ <View
211
+ accessibilityLabel={`${item.severity} attention: ${item.title}. ${item.message}`}
212
+ style={styles.attentionRow}
213
+ >
214
+ <View style={styles.rowSignal}>
215
+ <Icon name="TriangleAlert" size={15} color={statusColor(item.severity, theme.colors)} />
216
+ </View>
217
+ <View style={styles.rowBody}>
218
+ <View style={styles.rowTop}>
219
+ <Text style={styles.rowTitle} numberOfLines={1}>
220
+ {item.title}
221
+ </Text>
222
+ <Text style={[styles.badgeText, { color: statusColor(item.severity, theme.colors) }]}>
223
+ {item.severity}
224
+ </Text>
225
+ </View>
226
+ <Text style={styles.rowMessage} numberOfLines={2}>
227
+ {item.message}
228
+ </Text>
229
+ <Text style={styles.rowMeta}>
230
+ {item.kind} · {item.code} · {relativeTime(item.observedAt)}
231
+ </Text>
232
+ {onRespond ? (
233
+ <View style={styles.rowActions}>
234
+ <Pressable
235
+ accessibilityRole="button"
236
+ accessibilityLabel={`Respond to ${item.title}`}
237
+ onPress={onRespond}
238
+ style={({ pressed }) => [styles.inlineButton, pressed && styles.pressed]}
239
+ >
240
+ <Icon name="MessageSquareReply" size={13} color={theme.colors.foreground} />
241
+ <Text style={styles.inlineButtonText}>Respond</Text>
242
+ </Pressable>
243
+ </View>
244
+ ) : null}
245
+ </View>
246
+ </View>
247
+ );
248
+ }
249
+
250
+ function ConvoyRow({
251
+ item,
252
+ styles,
253
+ theme,
254
+ }: {
255
+ item: GasCityConvoy;
256
+ styles: Styles;
257
+ theme: PluginHostProps["theme"];
258
+ }) {
259
+ return (
260
+ <View
261
+ accessibilityLabel={`${item.title}. ${item.status}. ${convoyProgress(item)}.`}
262
+ style={styles.ledgerRow}
263
+ >
264
+ <View style={styles.rowSignal}>
265
+ <Icon
266
+ name="Workflow"
267
+ size={15}
268
+ color={rowSignalColor(item.blocked ? "blocked" : item.status, theme.colors)}
269
+ />
270
+ </View>
271
+ <View style={styles.rowBody}>
272
+ <View style={styles.rowTop}>
273
+ <Text style={styles.rowTitle} numberOfLines={1}>
274
+ {item.title}
275
+ </Text>
276
+ <Text style={styles.badgeText}>
277
+ {item.priority === null ? "P–" : `P${item.priority}`}
278
+ </Text>
279
+ </View>
280
+ <Text style={styles.rowMessage}>{convoyProgress(item)}</Text>
281
+ <Text style={styles.rowMeta}>
282
+ {item.id} · {item.assignee ?? "unassigned"} · {item.status}
283
+ </Text>
284
+ </View>
285
+ </View>
286
+ );
287
+ }
288
+
289
+ function WorkRow({
290
+ item,
291
+ styles,
292
+ theme,
293
+ }: {
294
+ item: GasCityWorkItem;
295
+ styles: Styles;
296
+ theme: PluginHostProps["theme"];
297
+ }) {
298
+ return (
299
+ <View
300
+ accessibilityLabel={`${item.title}. ${item.status}. ${item.type}.`}
301
+ style={styles.ledgerRow}
302
+ >
303
+ <View style={styles.rowSignal}>
304
+ <Icon
305
+ name="CircleDot"
306
+ size={15}
307
+ color={rowSignalColor(item.blocked ? "blocked" : item.status, theme.colors)}
308
+ />
309
+ </View>
310
+ <View style={styles.rowBody}>
311
+ <View style={styles.rowTop}>
312
+ <Text style={styles.rowTitle} numberOfLines={1}>
313
+ {item.title}
314
+ </Text>
315
+ <Text style={styles.badgeText}>
316
+ {item.priority === null ? "P–" : `P${item.priority}`}
317
+ </Text>
318
+ </View>
319
+ <Text style={styles.rowMessage} numberOfLines={1}>
320
+ {item.id} · {item.type}
321
+ </Text>
322
+ <Text style={styles.rowMeta}>
323
+ {item.assignee ?? "unassigned"} · {item.status}
324
+ </Text>
325
+ </View>
326
+ </View>
327
+ );
328
+ }
329
+
330
+ function EventRow({
331
+ item,
332
+ styles,
333
+ theme,
334
+ }: {
335
+ item: GasCityEvent;
336
+ styles: Styles;
337
+ theme: PluginHostProps["theme"];
338
+ }) {
339
+ return (
340
+ <View
341
+ accessibilityLabel={`${item.type}. ${item.message ?? "No message"}. ${relativeTime(item.timestamp)}.`}
342
+ style={styles.ledgerRow}
343
+ >
344
+ <View style={[styles.eventMarker, { borderColor: theme.colors.foregroundMuted }]} />
345
+ <View style={styles.rowBody}>
346
+ <View style={styles.rowTop}>
347
+ <Text style={styles.rowTitle} numberOfLines={1}>
348
+ {item.type}
349
+ </Text>
350
+ <Text style={styles.sequence}>#{item.sequence}</Text>
351
+ </View>
352
+ {item.message ? (
353
+ <Text style={styles.rowMessage} numberOfLines={2}>
354
+ {item.message}
355
+ </Text>
356
+ ) : null}
357
+ <Text style={styles.rowMeta}>
358
+ {item.actor ?? "system"} · {relativeTime(item.timestamp)}
359
+ </Text>
360
+ </View>
361
+ </View>
362
+ );
363
+ }
364
+
365
+ export function CityOperations({
366
+ theme,
367
+ layout,
368
+ host,
369
+ cityName,
370
+ rigName,
371
+ settings,
372
+ slingIntent,
373
+ onDismissSlingIntent,
374
+ }: CityOperationsProps) {
375
+ const styles = useMemo(() => createStyles(theme, layout.compact), [layout.compact, theme]);
376
+ const toast = useToast();
377
+ const queryClient = useQueryClient();
378
+ const loadSnapshot = useRpc(getCityRigSnapshot);
379
+ const loadSessions = useRpc(listSessions);
380
+ const loadConvoys = useRpc(listConvoys);
381
+ const loadWork = useRpc(listWork);
382
+ const loadEvents = useRpc(listEvents);
383
+ const loadAttention = useRpc(listAttention);
384
+ const runDispatch = useRpc(dispatchWork);
385
+ const runSessionAction = useRpc(performSessionAction);
386
+ const [dispatchOpen, setDispatchOpen] = useState(false);
387
+ const [dispatchBeadId, setDispatchBeadId] = useState("");
388
+ const [dispatchAgent, setDispatchAgent] = useState("");
389
+ const [dispatchDraftError, setDispatchDraftError] = useState<string | null>(null);
390
+ const [sessionDialog, setSessionDialog] = useState<SessionActionDialog | null>(null);
391
+ const [sessionAction, setSessionAction] = useState<SessionAction>("message");
392
+ const [sessionMessage, setSessionMessage] = useState("");
393
+ const [interactionResponse, setInteractionResponse] = useState<InteractionResponse>("allow");
394
+ const [eventCursor, setEventCursor] = useState<string | null>(null);
395
+ const rpcSettings = useMemo(() => toGasCityRpcSettings(settings), [settings]);
396
+
397
+ useEffect(() => {
398
+ if (!slingIntent) return;
399
+ setDispatchBeadId(slingIntent.beadId);
400
+ setDispatchAgent(slingIntent.agent);
401
+ setDispatchDraftError(slingIntent.parseError);
402
+ setDispatchOpen(true);
403
+ onDismissSlingIntent?.(slingIntent.id);
404
+ }, [onDismissSlingIntent, slingIntent]);
405
+
406
+ const scope = useMemo(
407
+ () => ({ settings: rpcSettings, cityName, rigName }),
408
+ [cityName, rigName, rpcSettings],
409
+ );
410
+ const queryRoot = useMemo(
411
+ () => cityQueryRoot(host.id, settings.endpointUrl, cityName, rigName),
412
+ [cityName, host.id, rigName, settings.endpointUrl],
413
+ );
414
+ const snapshotQuery = useQuery({
415
+ queryKey: [...queryRoot, "snapshot"],
416
+ queryFn: () => loadSnapshot(scope),
417
+ refetchInterval: settings.refreshIntervalMs,
418
+ });
419
+ const sessionsQuery = useQuery({
420
+ queryKey: [...queryRoot, "sessions"],
421
+ queryFn: () => loadSessions(scope),
422
+ refetchInterval: settings.refreshIntervalMs,
423
+ });
424
+ const convoysQuery = useQuery({
425
+ queryKey: [...queryRoot, "convoys"],
426
+ queryFn: () => loadConvoys(scope),
427
+ refetchInterval: settings.refreshIntervalMs,
428
+ });
429
+ const workQuery = useQuery({
430
+ queryKey: [...queryRoot, "work", settings.eventLimit],
431
+ queryFn: () => loadWork(scope),
432
+ refetchInterval: settings.refreshIntervalMs,
433
+ });
434
+ const eventsQuery = useQuery({
435
+ queryKey: [...queryRoot, "events", settings.eventLimit, eventCursor],
436
+ queryFn: () =>
437
+ loadEvents({ settings: rpcSettings, scope: "city", cityName, cursor: eventCursor }),
438
+ refetchInterval: settings.refreshIntervalMs,
439
+ });
440
+ const attentionQuery = useQuery({
441
+ queryKey: [...queryRoot, "attention"],
442
+ queryFn: () => loadAttention(scope),
443
+ refetchInterval: settings.refreshIntervalMs,
444
+ });
445
+
446
+ const baseSections = useMemo(
447
+ () =>
448
+ buildDashboardSections({
449
+ attention: attentionQuery.data,
450
+ sessions: sessionsQuery.data,
451
+ convoys: convoysQuery.data,
452
+ work: workQuery.data,
453
+ events: eventsQuery.data,
454
+ }),
455
+ [attentionQuery.data, convoysQuery.data, eventsQuery.data, sessionsQuery.data, workQuery.data],
456
+ );
457
+ const sections = useMemo(() => {
458
+ const queries = [attentionQuery, sessionsQuery, convoysQuery, workQuery, eventsQuery];
459
+ return baseSections.map((section, index) => {
460
+ const query = queries[index];
461
+ return presentSection(section, {
462
+ hasData: query.data !== undefined,
463
+ isPending: query.isPending,
464
+ isFetching: query.isFetching,
465
+ error: query.error,
466
+ });
467
+ });
468
+ }, [attentionQuery, baseSections, convoysQuery, eventsQuery, sessionsQuery, workQuery]);
469
+
470
+ const refresh = refreshPresentation({
471
+ hasData: snapshotQuery.data !== undefined,
472
+ isPending: snapshotQuery.isPending,
473
+ isFetching:
474
+ snapshotQuery.isFetching ||
475
+ sessionsQuery.isFetching ||
476
+ convoysQuery.isFetching ||
477
+ workQuery.isFetching ||
478
+ eventsQuery.isFetching ||
479
+ attentionQuery.isFetching,
480
+ error:
481
+ snapshotQuery.error ??
482
+ sessionsQuery.error ??
483
+ convoysQuery.error ??
484
+ workQuery.error ??
485
+ eventsQuery.error ??
486
+ attentionQuery.error,
487
+ refreshedAt: snapshotQuery.data?.refreshedAt,
488
+ });
489
+
490
+ const dispatchMutation = useMutation({
491
+ mutationFn: async () => {
492
+ if (!settings.mutationsEnabled) {
493
+ throw new Error("Enable mutations in Gas City settings first.");
494
+ }
495
+ const beadId = dispatchBeadId.trim();
496
+ const agent = dispatchAgent.trim();
497
+ if (!beadId || !agent) throw new Error("Bead ID and agent role are required.");
498
+ const request = {
499
+ kind: "bead",
500
+ confirmed: true,
501
+ target: { cityName, rigName, agent },
502
+ beadId,
503
+ reassign: false,
504
+ owned: false,
505
+ force: false,
506
+ noFormula: false,
507
+ noConvoy: false,
508
+ merge: "direct",
509
+ } satisfies DispatchRequest;
510
+ return runDispatch({ settings: rpcSettings, request });
511
+ },
512
+ onSuccess: (result) => {
513
+ toast.show(`Dispatched ${result.beadId ?? "work"} to ${result.target}.`, {
514
+ variant: "success",
515
+ });
516
+ setDispatchOpen(false);
517
+ setDispatchBeadId("");
518
+ setDispatchAgent("");
519
+ setDispatchDraftError(null);
520
+ },
521
+ onError: (error) => toast.error(errorMessage(error)),
522
+ onSettled: async () => {
523
+ await queryClient.invalidateQueries({ queryKey: queryRoot });
524
+ },
525
+ });
526
+
527
+ const sessionMutation = useMutation({
528
+ mutationFn: async () => {
529
+ if (!settings.mutationsEnabled) {
530
+ throw new Error("Enable mutations in Gas City settings first.");
531
+ }
532
+ if (!sessionDialog) throw new Error("Choose a session first.");
533
+ const base = { cityName, sessionId: sessionDialog.sessionId, confirmed: true as const };
534
+ let request: SessionActionRequest;
535
+ if (sessionAction === "message") {
536
+ request = { ...base, action: "message", message: sessionMessage.trim() };
537
+ } else if (sessionAction === "submit") {
538
+ request = {
539
+ ...base,
540
+ action: "submit",
541
+ message: sessionMessage.trim(),
542
+ intent: "follow_up",
543
+ };
544
+ } else if (sessionAction === "respond") {
545
+ if (!sessionDialog.requestId) throw new Error("Pending interaction request ID is missing.");
546
+ request = {
547
+ ...base,
548
+ action: "respond",
549
+ requestId: sessionDialog.requestId,
550
+ response: interactionResponse,
551
+ text: interactionResponse === "answer" ? sessionMessage.trim() : null,
552
+ metadata: {},
553
+ };
554
+ } else {
555
+ request = { ...base, action: sessionAction };
556
+ }
557
+ return runSessionAction({ settings: rpcSettings, request });
558
+ },
559
+ onSuccess: (result) => {
560
+ toast.show(`${sessionAction} accepted for ${result.sessionId}.`, { variant: "success" });
561
+ setSessionDialog(null);
562
+ setSessionMessage("");
563
+ },
564
+ onError: (error) => toast.error(errorMessage(error)),
565
+ onSettled: async () => {
566
+ await queryClient.invalidateQueries({ queryKey: queryRoot });
567
+ },
568
+ });
569
+
570
+ async function refreshAll() {
571
+ await Promise.all([
572
+ snapshotQuery.refetch(),
573
+ sessionsQuery.refetch(),
574
+ convoysQuery.refetch(),
575
+ workQuery.refetch(),
576
+ eventsQuery.refetch(),
577
+ attentionQuery.refetch(),
578
+ ]);
579
+ }
580
+
581
+ function openSessionActions(session: GasCitySession) {
582
+ const actions = sessionActionsFor(session);
583
+ const firstAction = actions[0];
584
+ if (!firstAction) return;
585
+ setSessionDialog({
586
+ sessionId: session.id,
587
+ title: session.title,
588
+ actions,
589
+ requestId: null,
590
+ });
591
+ setSessionAction(firstAction);
592
+ setSessionMessage("");
593
+ }
594
+
595
+ function openInteractionResponse(item: AttentionItem) {
596
+ if (!item.resourceId || !item.requestId) return;
597
+ setSessionDialog({
598
+ sessionId: item.resourceId,
599
+ title: item.title,
600
+ actions: ["respond"],
601
+ requestId: item.requestId,
602
+ });
603
+ setSessionAction("respond");
604
+ setInteractionResponse("allow");
605
+ setSessionMessage("");
606
+ }
607
+
608
+ function renderSession(item: GasCitySession) {
609
+ const actions = sessionActionsFor(item);
610
+ return (
611
+ <View accessibilityLabel={sessionAccessibilityLabel(item)} style={styles.row}>
612
+ <View style={styles.rowSignal}>
613
+ <Icon
614
+ name={item.running ? "Activity" : "CirclePause"}
615
+ size={15}
616
+ color={rowSignalColor(item.state, theme.colors)}
617
+ />
618
+ </View>
619
+ <View style={styles.rowBody}>
620
+ <View style={styles.rowTop}>
621
+ <Text style={styles.rowTitle} numberOfLines={1}>
622
+ {item.title}
623
+ </Text>
624
+ <Text style={[styles.badgeText, { color: rowSignalColor(item.state, theme.colors) }]}>
625
+ {item.state}
626
+ </Text>
627
+ </View>
628
+ <Text style={styles.rowMessage} numberOfLines={1}>
629
+ {item.sessionName} · {item.provider}
630
+ {item.model ? ` / ${item.model}` : ""}
631
+ </Text>
632
+ <Text style={styles.rowMeta}>
633
+ {item.rigName ?? "city"} · {item.activity ?? "idle"} · {relativeTime(item.lastActiveAt)}
634
+ </Text>
635
+ <View style={styles.rowActions}>
636
+ {actions.length > 0 ? (
637
+ <Pressable
638
+ accessibilityRole="button"
639
+ accessibilityLabel={`Open actions for ${item.title}`}
640
+ onPress={() => openSessionActions(item)}
641
+ style={({ pressed }) => [styles.inlineButton, pressed && styles.pressed]}
642
+ >
643
+ <Icon name="SlidersHorizontal" size={13} color={theme.colors.foreground} />
644
+ <Text style={styles.inlineButtonText}>Actions</Text>
645
+ </Pressable>
646
+ ) : null}
647
+ </View>
648
+ </View>
649
+ </View>
650
+ );
651
+ }
652
+
653
+ function renderRow({ item }: { item: DashboardRow }) {
654
+ if (item.kind === "status") {
655
+ return (
656
+ <View
657
+ accessibilityRole={item.tone === "error" || item.tone === "stale" ? "alert" : undefined}
658
+ style={styles.inlineState}
659
+ >
660
+ {item.tone === "loading" || item.tone === "refreshing" ? (
661
+ <ActivityIndicator size="small" color={theme.colors.accent} />
662
+ ) : (
663
+ <Icon
664
+ name="AlertTriangle"
665
+ size={14}
666
+ color={item.tone === "error" ? theme.colors.statusDanger : theme.colors.statusWarning}
667
+ />
668
+ )}
669
+ <Text style={styles.inlineStateText}>{item.message}</Text>
670
+ </View>
671
+ );
672
+ }
673
+ if (item.kind === "empty") return <Text style={styles.emptyText}>{item.message}</Text>;
674
+ if (item.kind === "attention") {
675
+ const canRespond = Boolean(item.item.requestId && item.item.resourceId);
676
+ return (
677
+ <AttentionRow
678
+ item={item.item}
679
+ onRespond={canRespond ? () => openInteractionResponse(item.item) : undefined}
680
+ styles={styles}
681
+ theme={theme}
682
+ />
683
+ );
684
+ }
685
+ if (item.kind === "session") return renderSession(item.item);
686
+ if (item.kind === "convoy") return <ConvoyRow item={item.item} styles={styles} theme={theme} />;
687
+ if (item.kind === "work") return <WorkRow item={item.item} styles={styles} theme={theme} />;
688
+ return <EventRow item={item.item} styles={styles} theme={theme} />;
689
+ }
690
+
691
+ const snapshot = snapshotQuery.data;
692
+ const sessionSummary = activeSessionSummary(sessionsQuery.data?.items);
693
+ let body: ReactNode;
694
+ if (snapshotQuery.isPending && !snapshot) {
695
+ body = (
696
+ <StateCard
697
+ title="Loading city operations"
698
+ body="Reading the city, rig, and work state."
699
+ loading
700
+ styles={styles}
701
+ theme={theme}
702
+ />
703
+ );
704
+ } else if (snapshotQuery.error && !snapshot) {
705
+ body = (
706
+ <StateCard
707
+ title="Could not load city operations"
708
+ body={errorMessage(snapshotQuery.error)}
709
+ onRetry={() => void refreshAll()}
710
+ styles={styles}
711
+ theme={theme}
712
+ />
713
+ );
714
+ } else if (!snapshot) {
715
+ body = (
716
+ <StateCard
717
+ title="No city snapshot"
718
+ body="The supervisor returned no usable city data."
719
+ onRetry={() => void refreshAll()}
720
+ styles={styles}
721
+ theme={theme}
722
+ />
723
+ );
724
+ } else {
725
+ const header = (
726
+ <View style={styles.summary}>
727
+ <View style={styles.summaryHeading}>
728
+ <View style={styles.summaryTitleBlock}>
729
+ <Text style={styles.eyebrow}>
730
+ {rigName ? "Mapped production line" : "City operations"}
731
+ </Text>
732
+ <Text accessibilityRole="header" style={styles.cityTitle}>
733
+ {cityName}
734
+ </Text>
735
+ <Text style={styles.summaryMeta}>
736
+ {snapshot.city.status ?? (snapshot.city.running ? "running" : "stopped")} ·{" "}
737
+ {rigName ?? `${snapshot.rigs.length} rigs`}
738
+ </Text>
739
+ </View>
740
+ <Pressable
741
+ accessibilityRole="button"
742
+ accessibilityLabel="Open confirmed Gas City dispatch"
743
+ onPress={() => setDispatchOpen(true)}
744
+ style={({ pressed }) => [styles.primaryButton, pressed && styles.pressed]}
745
+ >
746
+ <Icon name="Send" size={14} color={theme.colors.accentForeground} />
747
+ <Text style={styles.primaryButtonText}>Sling</Text>
748
+ </Pressable>
749
+ </View>
750
+ <ScrollView
751
+ style={styles.statsScroller}
752
+ horizontal
753
+ showsHorizontalScrollIndicator={false}
754
+ contentContainerStyle={styles.statsRail}
755
+ >
756
+ <Stat
757
+ label="city-wide agents"
758
+ value={`${snapshot.city.agents.running}/${snapshot.city.agents.total}`}
759
+ styles={styles}
760
+ />
761
+ <Stat label="city-wide sessions" value={snapshot.city.sessions.active} styles={styles} />
762
+ <Stat label="city-wide ready" value={snapshot.city.work.ready} styles={styles} />
763
+ <Stat
764
+ label="city-wide in progress"
765
+ value={snapshot.city.work.inProgress}
766
+ styles={styles}
767
+ />
768
+ <Stat label="city-wide open" value={snapshot.city.work.open} styles={styles} />
769
+ </ScrollView>
770
+ {sessionSummary ? <Text style={styles.sessionSummary}>{sessionSummary}</Text> : null}
771
+ {snapshot.rig ? (
772
+ <View style={styles.rigCard}>
773
+ <View>
774
+ <Text style={styles.rigName}>{snapshot.rig.name}</Text>
775
+ <Text style={styles.rowMeta} numberOfLines={1}>
776
+ {snapshot.rig.path}
777
+ {snapshot.rig.git
778
+ ? ` · ${snapshot.rig.git.branch}${snapshot.rig.git.clean ? "" : "*"}`
779
+ : ""}
780
+ </Text>
781
+ </View>
782
+ <Text
783
+ style={[
784
+ styles.badgeText,
785
+ {
786
+ color: statusColor(
787
+ snapshot.rig.suspended ? "suspended" : "running",
788
+ theme.colors,
789
+ ),
790
+ },
791
+ ]}
792
+ >
793
+ {snapshot.rig.suspended
794
+ ? "suspended"
795
+ : `${snapshot.rig.runningAgentCount}/${snapshot.rig.agentCount} agents`}
796
+ </Text>
797
+ </View>
798
+ ) : (
799
+ <ScrollView
800
+ horizontal
801
+ showsHorizontalScrollIndicator={false}
802
+ contentContainerStyle={styles.rigRail}
803
+ >
804
+ {snapshot.rigs.map((rig) => (
805
+ <View key={`${rig.path}:${rig.name}`} style={styles.rigPill}>
806
+ <View
807
+ style={[
808
+ styles.rigDot,
809
+ {
810
+ backgroundColor: statusColor(
811
+ rig.suspended ? "suspended" : "running",
812
+ theme.colors,
813
+ ),
814
+ },
815
+ ]}
816
+ />
817
+ <Text style={styles.rigPillText}>
818
+ {rig.name} · {rig.runningAgentCount}/{rig.agentCount}
819
+ </Text>
820
+ </View>
821
+ ))}
822
+ {snapshot.rigs.length === 0 ? (
823
+ <Text style={styles.emptyText}>No rigs reported.</Text>
824
+ ) : null}
825
+ </ScrollView>
826
+ )}
827
+ {snapshot.partial ? (
828
+ <View accessibilityRole="alert" style={styles.partialNotice}>
829
+ <Icon name="TriangleAlert" size={14} color={theme.colors.statusWarning} />
830
+ <Text style={styles.partialText}>
831
+ Partial snapshot. Some supervisor data is unavailable.
832
+ </Text>
833
+ </View>
834
+ ) : null}
835
+ <Diagnostics diagnostics={snapshot.diagnostics} styles={styles} theme={theme} />
836
+ </View>
837
+ );
838
+
839
+ body = (
840
+ <SectionList<DashboardRow, DashboardSection>
841
+ style={styles.list}
842
+ contentContainerStyle={styles.listContent}
843
+ sections={sections}
844
+ keyExtractor={(item, index) => {
845
+ if (item.kind === "attention") return `attention:${item.item.id}`;
846
+ if (item.kind === "session") return `session:${item.item.id}`;
847
+ if (item.kind === "convoy") return `convoy:${item.item.id}`;
848
+ if (item.kind === "work") return `work:${item.item.id}`;
849
+ if (item.kind === "event")
850
+ return `event:${item.item.cityName ?? "global"}:${item.item.sequence}`;
851
+ return `${item.kind}:${index}:${item.message}`;
852
+ }}
853
+ initialNumToRender={16}
854
+ maxToRenderPerBatch={12}
855
+ windowSize={7}
856
+ stickySectionHeadersEnabled={false}
857
+ ListHeaderComponent={header}
858
+ renderSectionHeader={({ section }) => (
859
+ <View style={styles.sectionHeader}>
860
+ <Text accessibilityRole="header" style={styles.sectionTitle}>
861
+ {section.title}
862
+ </Text>
863
+ {section.truncated ? <Text style={styles.truncated}>bounded result</Text> : null}
864
+ </View>
865
+ )}
866
+ renderItem={renderRow}
867
+ />
868
+ );
869
+ }
870
+
871
+ const messageRequired =
872
+ sessionAction === "message" ||
873
+ sessionAction === "submit" ||
874
+ (sessionAction === "respond" && interactionResponse === "answer");
875
+ const sessionConfirmDisabled =
876
+ !settings.mutationsEnabled ||
877
+ sessionMutation.isPending ||
878
+ (messageRequired && sessionMessage.trim().length === 0);
879
+
880
+ return (
881
+ <View style={styles.screen}>
882
+ <View style={styles.toolbar}>
883
+ <View style={styles.liveLabel}>
884
+ <View
885
+ style={[
886
+ styles.liveDot,
887
+ {
888
+ backgroundColor:
889
+ refresh.state === "error" || refresh.state === "stale"
890
+ ? theme.colors.statusWarning
891
+ : theme.colors.statusSuccess,
892
+ },
893
+ ]}
894
+ />
895
+ <Text style={styles.refreshText} numberOfLines={1}>
896
+ {refresh.label}
897
+ </Text>
898
+ </View>
899
+ {eventCursor ? (
900
+ <Pressable
901
+ accessibilityRole="button"
902
+ accessibilityLabel="Return to latest Gas City events"
903
+ onPress={() => setEventCursor(null)}
904
+ style={({ pressed }) => [styles.refreshButton, pressed && styles.pressed]}
905
+ >
906
+ <Icon name="History" size={14} color={theme.colors.foreground} />
907
+ {!layout.compact ? <Text style={styles.refreshButtonText}>Latest</Text> : null}
908
+ </Pressable>
909
+ ) : null}
910
+ {eventsQuery.data?.cursor ? (
911
+ <Pressable
912
+ accessibilityRole="button"
913
+ accessibilityLabel="Load older Gas City events"
914
+ onPress={() => setEventCursor(eventsQuery.data?.cursor ?? null)}
915
+ style={({ pressed }) => [styles.refreshButton, pressed && styles.pressed]}
916
+ >
917
+ <Icon name="ChevronDown" size={14} color={theme.colors.foreground} />
918
+ {!layout.compact ? <Text style={styles.refreshButtonText}>Older</Text> : null}
919
+ </Pressable>
920
+ ) : null}
921
+ <Pressable
922
+ accessibilityRole="button"
923
+ accessibilityLabel="Refresh all Gas City data"
924
+ onPress={() => void refreshAll()}
925
+ style={({ pressed }) => [styles.refreshButton, pressed && styles.pressed]}
926
+ >
927
+ <Icon
928
+ name="RefreshCw"
929
+ size={14}
930
+ color={refresh.state === "refreshing" ? theme.colors.accent : theme.colors.foreground}
931
+ />
932
+ {!layout.compact ? <Text style={styles.refreshButtonText}>Refresh</Text> : null}
933
+ </Pressable>
934
+ </View>
935
+ <View style={styles.body}>{body}</View>
936
+
937
+ <Modal
938
+ title="Confirm Gas City dispatch"
939
+ open={dispatchOpen}
940
+ onOpenChange={(open) => {
941
+ if (!open && !dispatchMutation.isPending) setDispatchOpen(false);
942
+ }}
943
+ icon={<Icon name="Send" size={18} color={theme.colors.foreground} />}
944
+ >
945
+ <Modal.Content>
946
+ <View style={styles.modalBody}>
947
+ <Text style={styles.modalCopy}>
948
+ Dispatch one bead to a generic Gas City agent role in {cityName}
949
+ {rigName ? ` / ${rigName}` : ""}.
950
+ </Text>
951
+ {!settings.mutationsEnabled ? (
952
+ <View accessibilityRole="alert" style={styles.lockedNotice}>
953
+ <Icon name="Lock" size={15} color={theme.colors.statusWarning} />
954
+ <Text style={styles.lockedText}>
955
+ Observe-only mode. Enable mutations in persisted Gas City settings to dispatch
956
+ work.
957
+ </Text>
958
+ </View>
959
+ ) : null}
960
+ {dispatchDraftError ? (
961
+ <Text accessibilityRole="alert" style={styles.errorText}>
962
+ {dispatchDraftError}
963
+ </Text>
964
+ ) : null}
965
+ <View style={styles.field}>
966
+ <Text style={styles.fieldLabel}>Bead ID</Text>
967
+ <TextInput
968
+ accessibilityLabel="Gas City bead ID"
969
+ autoCapitalize="none"
970
+ autoCorrect={false}
971
+ onChangeText={(value) => {
972
+ setDispatchBeadId(value);
973
+ setDispatchDraftError(null);
974
+ }}
975
+ placeholder="gc-123"
976
+ placeholderTextColor={theme.colors.foregroundMuted}
977
+ value={dispatchBeadId}
978
+ style={styles.textInput}
979
+ />
980
+ </View>
981
+ <View style={styles.field}>
982
+ <Text style={styles.fieldLabel}>Agent role</Text>
983
+ <TextInput
984
+ accessibilityLabel="Gas City agent role"
985
+ autoCapitalize="none"
986
+ autoCorrect={false}
987
+ onChangeText={(value) => {
988
+ setDispatchAgent(value);
989
+ setDispatchDraftError(null);
990
+ }}
991
+ placeholder="city/role"
992
+ placeholderTextColor={theme.colors.foregroundMuted}
993
+ value={dispatchAgent}
994
+ style={styles.textInput}
995
+ />
996
+ </View>
997
+ <View style={styles.modalActions}>
998
+ <Pressable
999
+ accessibilityRole="button"
1000
+ disabled={dispatchMutation.isPending}
1001
+ onPress={() => setDispatchOpen(false)}
1002
+ style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressed]}
1003
+ >
1004
+ <Text style={styles.secondaryButtonText}>Cancel</Text>
1005
+ </Pressable>
1006
+ <Pressable
1007
+ accessibilityRole="button"
1008
+ accessibilityLabel="Confirm Gas City dispatch"
1009
+ disabled={
1010
+ !settings.mutationsEnabled ||
1011
+ dispatchMutation.isPending ||
1012
+ !dispatchBeadId.trim() ||
1013
+ !dispatchAgent.trim()
1014
+ }
1015
+ onPress={() => dispatchMutation.mutate()}
1016
+ style={({ pressed }) => [
1017
+ styles.primaryButton,
1018
+ pressed && styles.pressed,
1019
+ (!settings.mutationsEnabled ||
1020
+ dispatchMutation.isPending ||
1021
+ !dispatchBeadId.trim() ||
1022
+ !dispatchAgent.trim()) &&
1023
+ styles.disabled,
1024
+ ]}
1025
+ >
1026
+ <Text style={styles.primaryButtonText}>
1027
+ {dispatchMutation.isPending ? "Dispatching…" : "Confirm dispatch"}
1028
+ </Text>
1029
+ </Pressable>
1030
+ </View>
1031
+ </View>
1032
+ </Modal.Content>
1033
+ </Modal>
1034
+
1035
+ <Modal
1036
+ title={sessionDialog ? `Confirm action: ${sessionDialog.title}` : "Confirm session action"}
1037
+ open={sessionDialog !== null}
1038
+ onOpenChange={(open) => {
1039
+ if (!open && !sessionMutation.isPending) setSessionDialog(null);
1040
+ }}
1041
+ icon={<Icon name="SlidersHorizontal" size={18} color={theme.colors.foreground} />}
1042
+ >
1043
+ <Modal.Content>
1044
+ <View style={styles.modalBody}>
1045
+ {!settings.mutationsEnabled ? (
1046
+ <View accessibilityRole="alert" style={styles.lockedNotice}>
1047
+ <Icon name="Lock" size={15} color={theme.colors.statusWarning} />
1048
+ <Text style={styles.lockedText}>
1049
+ Observe-only mode. Enable mutations in persisted Gas City settings to operate
1050
+ sessions.
1051
+ </Text>
1052
+ </View>
1053
+ ) : null}
1054
+ <ScrollView
1055
+ horizontal
1056
+ showsHorizontalScrollIndicator={false}
1057
+ contentContainerStyle={styles.actionRail}
1058
+ >
1059
+ {(sessionDialog?.actions ?? []).map((action) => {
1060
+ const selected = sessionAction === action;
1061
+ const label = SESSION_ACTION_LABELS[action];
1062
+ return (
1063
+ <Pressable
1064
+ key={action}
1065
+ accessibilityRole="button"
1066
+ accessibilityLabel={`${label} session`}
1067
+ accessibilityState={{ selected }}
1068
+ onPress={() => setSessionAction(action)}
1069
+ style={({ pressed }) => [
1070
+ styles.actionChip,
1071
+ selected && styles.actionChipSelected,
1072
+ action === "kill" && selected && styles.dangerChip,
1073
+ pressed && styles.pressed,
1074
+ ]}
1075
+ >
1076
+ <Text
1077
+ style={[styles.actionChipText, selected && styles.actionChipTextSelected]}
1078
+ >
1079
+ {label}
1080
+ </Text>
1081
+ </Pressable>
1082
+ );
1083
+ })}
1084
+ </ScrollView>
1085
+ {sessionAction === "respond" ? (
1086
+ <View style={styles.actionRail}>
1087
+ {(["allow", "deny", "answer"] as const).map((response) => {
1088
+ const selected = interactionResponse === response;
1089
+ return (
1090
+ <Pressable
1091
+ key={response}
1092
+ accessibilityRole="button"
1093
+ accessibilityLabel={`${response} pending interaction`}
1094
+ accessibilityState={{ selected }}
1095
+ onPress={() => setInteractionResponse(response)}
1096
+ style={[styles.actionChip, selected && styles.actionChipSelected]}
1097
+ >
1098
+ <Text
1099
+ style={[styles.actionChipText, selected && styles.actionChipTextSelected]}
1100
+ >
1101
+ {response}
1102
+ </Text>
1103
+ </Pressable>
1104
+ );
1105
+ })}
1106
+ </View>
1107
+ ) : null}
1108
+ {messageRequired ? (
1109
+ <View style={styles.field}>
1110
+ <Text style={styles.fieldLabel}>
1111
+ {sessionAction === "submit"
1112
+ ? "Follow-up prompt"
1113
+ : sessionAction === "respond"
1114
+ ? "Response"
1115
+ : "Message"}
1116
+ </Text>
1117
+ <TextInput
1118
+ accessibilityLabel={
1119
+ sessionAction === "submit"
1120
+ ? "Session follow-up prompt"
1121
+ : sessionAction === "respond"
1122
+ ? "Interaction response"
1123
+ : "Session message"
1124
+ }
1125
+ multiline
1126
+ onChangeText={setSessionMessage}
1127
+ placeholder="Give the session its next instruction"
1128
+ placeholderTextColor={theme.colors.foregroundMuted}
1129
+ value={sessionMessage}
1130
+ style={[styles.textInput, styles.multilineInput]}
1131
+ />
1132
+ </View>
1133
+ ) : (
1134
+ <Text style={styles.modalCopy}>
1135
+ This sends a confirmed {sessionAction} action to the selected Gas City session.
1136
+ </Text>
1137
+ )}
1138
+ <View style={styles.modalActions}>
1139
+ <Pressable
1140
+ accessibilityRole="button"
1141
+ disabled={sessionMutation.isPending}
1142
+ onPress={() => setSessionDialog(null)}
1143
+ style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressed]}
1144
+ >
1145
+ <Text style={styles.secondaryButtonText}>Cancel</Text>
1146
+ </Pressable>
1147
+ <Pressable
1148
+ accessibilityRole="button"
1149
+ accessibilityLabel={`Confirm ${sessionAction} session action`}
1150
+ disabled={sessionConfirmDisabled}
1151
+ onPress={() => sessionMutation.mutate()}
1152
+ style={({ pressed }) => [
1153
+ styles.primaryButton,
1154
+ sessionAction === "kill" && styles.dangerButton,
1155
+ pressed && styles.pressed,
1156
+ sessionConfirmDisabled && styles.disabled,
1157
+ ]}
1158
+ >
1159
+ <Text style={styles.primaryButtonText}>
1160
+ {sessionMutation.isPending ? "Working…" : `Confirm ${sessionAction}`}
1161
+ </Text>
1162
+ </Pressable>
1163
+ </View>
1164
+ </View>
1165
+ </Modal.Content>
1166
+ </Modal>
1167
+ </View>
1168
+ );
1169
+ }
1170
+
1171
+ function createStyles(theme: PluginHostProps["theme"], compact: boolean) {
1172
+ return StyleSheet.create({
1173
+ screen: { flex: 1, backgroundColor: theme.colors.surface0 },
1174
+ toolbar: {
1175
+ minHeight: 42,
1176
+ paddingHorizontal: compact ? 12 : 18,
1177
+ flexDirection: "row",
1178
+ alignItems: "center",
1179
+ justifyContent: "space-between",
1180
+ gap: 10,
1181
+ borderBottomWidth: StyleSheet.hairlineWidth,
1182
+ borderBottomColor: theme.colors.border,
1183
+ backgroundColor: theme.colors.surface0,
1184
+ },
1185
+ liveLabel: { minWidth: 0, flex: 1, flexDirection: "row", alignItems: "center", gap: 7 },
1186
+ liveDot: { width: 7, height: 7, borderRadius: 4 },
1187
+ refreshText: { flex: 1, color: theme.colors.foregroundMuted, fontSize: 11 },
1188
+ refreshButton: {
1189
+ minHeight: 32,
1190
+ flexDirection: "row",
1191
+ alignItems: "center",
1192
+ justifyContent: "center",
1193
+ gap: 6,
1194
+ paddingHorizontal: 9,
1195
+ borderRadius: 7,
1196
+ borderWidth: StyleSheet.hairlineWidth,
1197
+ borderColor: theme.colors.border,
1198
+ backgroundColor: theme.colors.surface2,
1199
+ },
1200
+ refreshButtonText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
1201
+ body: { flex: 1 },
1202
+ list: { flex: 1 },
1203
+ listContent: { paddingBottom: 24 },
1204
+ summary: {
1205
+ padding: compact ? 12 : 18,
1206
+ gap: 12,
1207
+ borderBottomWidth: StyleSheet.hairlineWidth,
1208
+ borderBottomColor: theme.colors.border,
1209
+ },
1210
+ summaryHeading: {
1211
+ flexDirection: "row",
1212
+ alignItems: "center",
1213
+ justifyContent: "space-between",
1214
+ gap: 12,
1215
+ },
1216
+ summaryTitleBlock: { flex: 1, minWidth: 0, gap: 2 },
1217
+ eyebrow: {
1218
+ color: theme.colors.foregroundMuted,
1219
+ fontSize: 10,
1220
+ fontWeight: "700",
1221
+ letterSpacing: 1,
1222
+ textTransform: "uppercase",
1223
+ },
1224
+ cityTitle: {
1225
+ color: theme.colors.foreground,
1226
+ fontSize: compact ? 20 : 24,
1227
+ fontWeight: "800",
1228
+ letterSpacing: -0.4,
1229
+ },
1230
+ summaryMeta: { color: theme.colors.foregroundMuted, fontSize: 12 },
1231
+ sessionSummary: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 18 },
1232
+ statsScroller: { flexGrow: 0 },
1233
+ statsRail: {
1234
+ gap: 0,
1235
+ borderTopWidth: StyleSheet.hairlineWidth,
1236
+ borderBottomWidth: StyleSheet.hairlineWidth,
1237
+ borderColor: theme.colors.border,
1238
+ },
1239
+ stat: {
1240
+ minWidth: compact ? 78 : 94,
1241
+ gap: 2,
1242
+ paddingVertical: 9,
1243
+ paddingHorizontal: 11,
1244
+ borderRightWidth: StyleSheet.hairlineWidth,
1245
+ borderColor: theme.colors.border,
1246
+ },
1247
+ statValue: {
1248
+ color: theme.colors.foreground,
1249
+ fontSize: 17,
1250
+ fontWeight: "800",
1251
+ fontVariant: ["tabular-nums"],
1252
+ },
1253
+ statLabel: {
1254
+ color: theme.colors.foregroundMuted,
1255
+ fontSize: 10,
1256
+ fontWeight: "600",
1257
+ letterSpacing: 0.7,
1258
+ textTransform: "uppercase",
1259
+ },
1260
+ rigCard: {
1261
+ flexDirection: "row",
1262
+ alignItems: "center",
1263
+ justifyContent: "space-between",
1264
+ gap: 12,
1265
+ padding: 10,
1266
+ borderRadius: 6,
1267
+ borderWidth: StyleSheet.hairlineWidth,
1268
+ borderColor: theme.colors.border,
1269
+ backgroundColor: theme.colors.surface0,
1270
+ },
1271
+ rigName: { color: theme.colors.foreground, fontSize: 13, fontWeight: "700" },
1272
+ rigRail: { gap: 7 },
1273
+ rigPill: {
1274
+ flexDirection: "row",
1275
+ alignItems: "center",
1276
+ gap: 6,
1277
+ paddingVertical: 7,
1278
+ paddingHorizontal: 9,
1279
+ borderRadius: 6,
1280
+ borderWidth: StyleSheet.hairlineWidth,
1281
+ borderColor: theme.colors.border,
1282
+ backgroundColor: theme.colors.surface0,
1283
+ },
1284
+ rigDot: { width: 6, height: 6, borderRadius: 3 },
1285
+ rigPillText: { color: theme.colors.foreground, fontSize: 11, fontWeight: "600" },
1286
+ partialNotice: {
1287
+ flexDirection: "row",
1288
+ alignItems: "center",
1289
+ gap: 7,
1290
+ padding: 9,
1291
+ borderRadius: 7,
1292
+ borderWidth: StyleSheet.hairlineWidth,
1293
+ borderColor: theme.colors.statusWarning,
1294
+ },
1295
+ partialText: { flex: 1, color: theme.colors.statusWarning, fontSize: 11 },
1296
+ diagnostics: {
1297
+ gap: 5,
1298
+ padding: 10,
1299
+ borderRadius: 6,
1300
+ borderWidth: StyleSheet.hairlineWidth,
1301
+ borderColor: theme.colors.border,
1302
+ backgroundColor: theme.colors.surface0,
1303
+ },
1304
+ diagnosticsTitleRow: { flexDirection: "row", alignItems: "center", gap: 6 },
1305
+ diagnosticsTitle: {
1306
+ color: theme.colors.foreground,
1307
+ fontSize: 10,
1308
+ fontWeight: "700",
1309
+ letterSpacing: 0.9,
1310
+ textTransform: "uppercase",
1311
+ },
1312
+ diagnosticText: { color: theme.colors.foregroundMuted, fontSize: 11, lineHeight: 16 },
1313
+ sectionHeader: {
1314
+ flexDirection: "row",
1315
+ alignItems: "center",
1316
+ justifyContent: "space-between",
1317
+ paddingHorizontal: compact ? 12 : 18,
1318
+ paddingTop: 18,
1319
+ paddingBottom: 7,
1320
+ backgroundColor: theme.colors.surface0,
1321
+ },
1322
+ sectionTitle: {
1323
+ color: theme.colors.foreground,
1324
+ fontSize: 10,
1325
+ fontWeight: "700",
1326
+ letterSpacing: 0.9,
1327
+ textTransform: "uppercase",
1328
+ },
1329
+ truncated: { color: theme.colors.statusWarning, fontSize: 10, textTransform: "uppercase" },
1330
+ row: {
1331
+ marginHorizontal: compact ? 12 : 18,
1332
+ marginBottom: 8,
1333
+ minHeight: 70,
1334
+ flexDirection: "row",
1335
+ borderRadius: 6,
1336
+ borderWidth: StyleSheet.hairlineWidth,
1337
+ borderColor: theme.colors.border,
1338
+ backgroundColor: theme.colors.surface1,
1339
+ },
1340
+ attentionRow: {
1341
+ marginHorizontal: compact ? 12 : 18,
1342
+ minHeight: 68,
1343
+ flexDirection: "row",
1344
+ borderBottomWidth: StyleSheet.hairlineWidth,
1345
+ borderColor: theme.colors.border,
1346
+ },
1347
+ ledgerRow: {
1348
+ marginHorizontal: compact ? 12 : 18,
1349
+ minHeight: 62,
1350
+ flexDirection: "row",
1351
+ borderBottomWidth: StyleSheet.hairlineWidth,
1352
+ borderColor: theme.colors.border,
1353
+ },
1354
+ rowSignal: { width: 30, alignItems: "center", paddingTop: 13, paddingLeft: 6 },
1355
+ eventMarker: {
1356
+ width: 8,
1357
+ height: 8,
1358
+ marginLeft: 11,
1359
+ marginTop: 15,
1360
+ borderRadius: 999,
1361
+ borderWidth: 1,
1362
+ },
1363
+ rowBody: { flex: 1, minWidth: 0, gap: 3, padding: 10 },
1364
+ rowTop: {
1365
+ flexDirection: "row",
1366
+ alignItems: "center",
1367
+ justifyContent: "space-between",
1368
+ gap: 10,
1369
+ },
1370
+ rowTitle: { flex: 1, color: theme.colors.foreground, fontSize: 13, fontWeight: "700" },
1371
+ rowMessage: { color: theme.colors.foreground, fontSize: 11, lineHeight: 16 },
1372
+ rowMeta: { color: theme.colors.foregroundMuted, fontSize: 10 },
1373
+ badgeText: {
1374
+ color: theme.colors.foregroundMuted,
1375
+ fontSize: 10,
1376
+ fontWeight: "700",
1377
+ letterSpacing: 0.6,
1378
+ textTransform: "uppercase",
1379
+ },
1380
+ sequence: { color: theme.colors.foregroundMuted, fontSize: 10, fontVariant: ["tabular-nums"] },
1381
+ rowActions: { flexDirection: "row", flexWrap: "wrap", gap: 6, paddingTop: 5 },
1382
+ inlineButton: {
1383
+ minHeight: 30,
1384
+ flexDirection: "row",
1385
+ alignItems: "center",
1386
+ gap: 5,
1387
+ paddingHorizontal: 8,
1388
+ borderRadius: 6,
1389
+ borderWidth: StyleSheet.hairlineWidth,
1390
+ borderColor: theme.colors.border,
1391
+ backgroundColor: theme.colors.surface2,
1392
+ },
1393
+ inlineButtonText: { color: theme.colors.foreground, fontSize: 10, fontWeight: "600" },
1394
+ inlineState: {
1395
+ marginHorizontal: compact ? 12 : 18,
1396
+ flexDirection: "row",
1397
+ alignItems: "center",
1398
+ gap: 8,
1399
+ paddingVertical: 12,
1400
+ borderBottomWidth: StyleSheet.hairlineWidth,
1401
+ borderColor: theme.colors.border,
1402
+ },
1403
+ inlineStateText: { flex: 1, color: theme.colors.foregroundMuted, fontSize: 11 },
1404
+ emptyText: {
1405
+ marginHorizontal: compact ? 12 : 18,
1406
+ paddingVertical: 16,
1407
+ color: theme.colors.foregroundMuted,
1408
+ fontSize: 12,
1409
+ textAlign: "center",
1410
+ borderBottomWidth: StyleSheet.hairlineWidth,
1411
+ borderColor: theme.colors.border,
1412
+ },
1413
+ stateCard: {
1414
+ margin: compact ? 12 : 18,
1415
+ padding: 22,
1416
+ alignItems: "center",
1417
+ gap: 8,
1418
+ borderRadius: 6,
1419
+ borderWidth: StyleSheet.hairlineWidth,
1420
+ borderColor: theme.colors.border,
1421
+ backgroundColor: theme.colors.surface1,
1422
+ },
1423
+ stateTitle: {
1424
+ color: theme.colors.foreground,
1425
+ fontSize: 16,
1426
+ fontWeight: "700",
1427
+ textAlign: "center",
1428
+ },
1429
+ stateBody: {
1430
+ color: theme.colors.foregroundMuted,
1431
+ fontSize: 12,
1432
+ lineHeight: 18,
1433
+ textAlign: "center",
1434
+ },
1435
+ primaryButton: {
1436
+ minHeight: 36,
1437
+ flexDirection: "row",
1438
+ alignItems: "center",
1439
+ justifyContent: "center",
1440
+ gap: 6,
1441
+ paddingHorizontal: 12,
1442
+ borderRadius: 7,
1443
+ backgroundColor: theme.colors.accent,
1444
+ },
1445
+ primaryButtonText: { color: theme.colors.accentForeground, fontSize: 12, fontWeight: "700" },
1446
+ secondaryButton: {
1447
+ minHeight: 36,
1448
+ alignItems: "center",
1449
+ justifyContent: "center",
1450
+ paddingHorizontal: 12,
1451
+ borderRadius: 7,
1452
+ borderWidth: StyleSheet.hairlineWidth,
1453
+ borderColor: theme.colors.border,
1454
+ backgroundColor: theme.colors.surface1,
1455
+ },
1456
+ secondaryButtonText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
1457
+ pressed: { opacity: 0.72 },
1458
+ disabled: { opacity: 0.42 },
1459
+ modalBody: { gap: 14 },
1460
+ modalCopy: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 18 },
1461
+ modalActions: { flexDirection: "row", justifyContent: "flex-end", flexWrap: "wrap", gap: 8 },
1462
+ field: { gap: 6 },
1463
+ fieldLabel: { color: theme.colors.foreground, fontSize: 11, fontWeight: "700" },
1464
+ textInput: {
1465
+ minHeight: 40,
1466
+ paddingHorizontal: 11,
1467
+ paddingVertical: 9,
1468
+ color: theme.colors.foreground,
1469
+ borderRadius: 7,
1470
+ borderWidth: StyleSheet.hairlineWidth,
1471
+ borderColor: theme.colors.border,
1472
+ backgroundColor: theme.colors.surface1,
1473
+ },
1474
+ multilineInput: { minHeight: 96, textAlignVertical: "top" },
1475
+ lockedNotice: {
1476
+ flexDirection: "row",
1477
+ alignItems: "flex-start",
1478
+ gap: 8,
1479
+ padding: 10,
1480
+ borderRadius: 7,
1481
+ borderWidth: StyleSheet.hairlineWidth,
1482
+ borderColor: theme.colors.statusWarning,
1483
+ },
1484
+ lockedText: { flex: 1, color: theme.colors.statusWarning, fontSize: 11, lineHeight: 16 },
1485
+ errorText: { color: theme.colors.statusDanger, fontSize: 11 },
1486
+ actionRail: { gap: 6 },
1487
+ actionChip: {
1488
+ minHeight: 34,
1489
+ justifyContent: "center",
1490
+ paddingHorizontal: 10,
1491
+ borderRadius: 7,
1492
+ borderWidth: StyleSheet.hairlineWidth,
1493
+ borderColor: theme.colors.border,
1494
+ backgroundColor: theme.colors.surface1,
1495
+ },
1496
+ actionChipSelected: {
1497
+ borderColor: theme.colors.accent,
1498
+ backgroundColor: theme.colors.surface2,
1499
+ },
1500
+ dangerChip: { borderColor: theme.colors.statusDanger },
1501
+ actionChipText: { color: theme.colors.foregroundMuted, fontSize: 11, fontWeight: "600" },
1502
+ actionChipTextSelected: { color: theme.colors.foreground },
1503
+ dangerButton: { backgroundColor: theme.colors.statusDanger },
1504
+ });
1505
+ }