yaver-feedback-react-native 0.8.12 → 0.9.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.
@@ -12,6 +12,7 @@ import {
12
12
  Text,
13
13
  TextInput,
14
14
  View,
15
+ useWindowDimensions,
15
16
  } from 'react-native';
16
17
  import { YaverFeedback } from './YaverFeedback';
17
18
  import {
@@ -24,13 +25,25 @@ import {
24
25
  // stopVideoRecording,
25
26
  } from './capture';
26
27
  import { uploadFeedback } from './upload';
27
- import { DeviceInfo, FeedbackBundle } from './types';
28
+ import {
29
+ DeviceInfo,
30
+ FeedbackBundle,
31
+ OpenCodeConfigSummary,
32
+ OpenCodeProviderSummary,
33
+ RunnerAuthStatusRow,
34
+ } from './types';
28
35
  import { AuthOverlay } from './AuthOverlay';
29
36
  import { QuickActionIcon } from './QuickActionIcon';
37
+ import { VibeChatScreen } from './VibeChatScreen';
38
+ import { DeployPanel } from './DeployPanel';
30
39
  import { listReachableDevices, RemoteDevice } from './auth';
31
40
  import {
32
41
  QUICK_ICON_COLOR_PRESETS,
33
42
  QuickIconColorPreset,
43
+ getPreferredModel,
44
+ getPreferredRunner,
45
+ setPreferredModel,
46
+ setPreferredRunner,
34
47
  } from './preferences';
35
48
 
36
49
  /**
@@ -61,7 +74,117 @@ type MachineCardState = {
61
74
  detail: string;
62
75
  };
63
76
 
77
+ type RunnerTone = 'ok' | 'warning' | 'error' | 'neutral';
78
+
79
+ type RunnerCardState = {
80
+ id: string;
81
+ name: string;
82
+ installed: boolean;
83
+ authConfigured: boolean;
84
+ ready: boolean;
85
+ version?: string;
86
+ tone: RunnerTone;
87
+ statusLine: string;
88
+ detail?: string;
89
+ actionLabel?: string;
90
+ actionRunner?: string;
91
+ };
92
+
93
+ type ProviderEditorState = {
94
+ mode: 'add' | 'edit';
95
+ id: string;
96
+ name: string;
97
+ baseUrl: string;
98
+ apiKey: string;
99
+ };
100
+
101
+ const PRIMARY_RUNNER_IDS = ['claude', 'codex', 'opencode'] as const;
102
+
103
+ function normalizeRunnerStatusRows(rows: RunnerAuthStatusRow[]): RunnerCardState[] {
104
+ const byId = new Map<string, RunnerAuthStatusRow>();
105
+ for (const row of rows) {
106
+ const raw = String(row.id || '').trim().toLowerCase();
107
+ if (!raw) continue;
108
+ const normalized = raw === 'claude-code' ? 'claude' : raw;
109
+ if (!PRIMARY_RUNNER_IDS.includes(normalized as (typeof PRIMARY_RUNNER_IDS)[number])) continue;
110
+ byId.set(normalized, { ...row, id: normalized });
111
+ }
112
+
113
+ return PRIMARY_RUNNER_IDS.map((id) => {
114
+ const baseName =
115
+ id === 'claude' ? 'Claude Code' : id === 'codex' ? 'OpenAI Codex' : 'OpenCode';
116
+ const row = byId.get(id);
117
+ if (!row) {
118
+ return {
119
+ id,
120
+ name: baseName,
121
+ installed: false,
122
+ authConfigured: false,
123
+ ready: false,
124
+ tone: 'warning',
125
+ statusLine: 'Not installed on the selected machine',
126
+ };
127
+ }
128
+
129
+ const versionPrefix = row.version?.trim() ? `${row.version.trim()} · ` : '';
130
+ const detail = row.error?.trim() || row.warning?.trim() || row.detail?.trim() || undefined;
131
+
132
+ if (!row.installed) {
133
+ return {
134
+ id,
135
+ name: row.name || baseName,
136
+ installed: false,
137
+ authConfigured: false,
138
+ ready: false,
139
+ version: row.version,
140
+ tone: 'warning',
141
+ statusLine: 'Not installed on the selected machine',
142
+ detail,
143
+ };
144
+ }
145
+
146
+ if (id === 'opencode') {
147
+ const configured = row.authConfigured || row.ready;
148
+ return {
149
+ id,
150
+ name: row.name || baseName,
151
+ installed: row.installed,
152
+ authConfigured: row.authConfigured,
153
+ ready: row.ready,
154
+ version: row.version,
155
+ tone: configured ? 'ok' : 'warning',
156
+ statusLine: configured
157
+ ? `${versionPrefix}Configured on the selected machine`
158
+ : `${versionPrefix}Needs provider config on the selected machine`,
159
+ detail,
160
+ };
161
+ }
162
+
163
+ const authed = row.authConfigured || row.ready;
164
+ return {
165
+ id,
166
+ name: row.name || baseName,
167
+ installed: row.installed,
168
+ authConfigured: row.authConfigured,
169
+ ready: row.ready,
170
+ version: row.version,
171
+ tone: authed ? 'ok' : 'warning',
172
+ statusLine: authed
173
+ ? `${versionPrefix}Signed in on the selected machine`
174
+ : `${versionPrefix}Not signed in on the selected machine`,
175
+ detail,
176
+ actionLabel: authed ? 'Re-auth' : 'Sign in',
177
+ actionRunner: id,
178
+ };
179
+ });
180
+ }
181
+
64
182
  export const FeedbackModal: React.FC = () => {
183
+ const { width: winW, height: winH } = useWindowDimensions();
184
+ const isTablet = Math.min(winW, winH) >= 600;
185
+ // Tablet color/icon picker fans out to 5/6 cols — 31% (3-col)
186
+ // looks empty on a 1024pt iPad. Mobile keeps 3-col.
187
+ const iconOptionWidthOverride = isTablet ? '18%' : undefined;
65
188
  const [visible, setVisible] = useState(false);
66
189
  const [action, setAction] = useState<ActionState>('idle');
67
190
  const [error, setError] = useState<string | null>(null);
@@ -79,6 +202,7 @@ export const FeedbackModal: React.FC = () => {
79
202
  // "pick something for me" prompt (which in 0.7.13 pointed Claude at
80
203
  // the wrong project because the matcher grepped the prompt itself).
81
204
  const [showVibeInput, setShowVibeInput] = useState(false);
205
+ const [showDeploy, setShowDeploy] = useState(false);
82
206
  const [vibePrompt, setVibePrompt] = useState('');
83
207
  const [lastVibeTaskId, setLastVibeTaskId] = useState<string | null>(null);
84
208
  const [quickIconColorPreset, setQuickIconColorPreset] =
@@ -92,6 +216,14 @@ export const FeedbackModal: React.FC = () => {
92
216
  title: 'No machine selected',
93
217
  detail: 'Pick a remote dev machine before using the feedback actions.',
94
218
  });
219
+ const [runnerCards, setRunnerCards] = useState<RunnerCardState[]>(() =>
220
+ normalizeRunnerStatusRows([]),
221
+ );
222
+ const [runnerStatusLoading, setRunnerStatusLoading] = useState(false);
223
+ const [runnerStatusError, setRunnerStatusError] = useState<string | null>(null);
224
+ const [preferredRunner, setPreferredRunnerState] = useState<string | null>(null);
225
+ const [preferredModel, setPreferredModelState] = useState('');
226
+ const [showOpenCodeConfig, setShowOpenCodeConfig] = useState(false);
95
227
  const mountedRef = useRef(true);
96
228
 
97
229
  const loadSelectedMachine = useCallback(async () => {
@@ -195,6 +327,69 @@ export const FeedbackModal: React.FC = () => {
195
327
  }
196
328
  }, []);
197
329
 
330
+ const loadRunnerStatuses = useCallback(async () => {
331
+ const cfg = YaverFeedback.getConfig();
332
+ if (!cfg?.authToken) {
333
+ if (mountedRef.current) {
334
+ setRunnerCards(normalizeRunnerStatusRows([]));
335
+ setRunnerStatusError('Sign in to inspect coding-agent status.');
336
+ setRunnerStatusLoading(false);
337
+ }
338
+ return;
339
+ }
340
+ if (!cfg.preferredDeviceId) {
341
+ if (mountedRef.current) {
342
+ setRunnerCards(normalizeRunnerStatusRows([]));
343
+ setRunnerStatusError('Pick a machine to inspect coding-agent status.');
344
+ setRunnerStatusLoading(false);
345
+ }
346
+ return;
347
+ }
348
+
349
+ if (mountedRef.current) {
350
+ setRunnerStatusLoading(true);
351
+ setRunnerStatusError(null);
352
+ }
353
+
354
+ try {
355
+ let client = YaverFeedback.getP2PClient();
356
+ if (!client) {
357
+ const ok = await YaverFeedback.reconnect();
358
+ if (ok) client = YaverFeedback.getP2PClient();
359
+ }
360
+ if (!client) {
361
+ throw new Error('Not connected to the selected machine yet.');
362
+ }
363
+ const rows = await client.getRunnerAuthStatus();
364
+ if (mountedRef.current) {
365
+ setRunnerCards(normalizeRunnerStatusRows(rows));
366
+ }
367
+ } catch (err) {
368
+ if (mountedRef.current) {
369
+ setRunnerCards(normalizeRunnerStatusRows([]));
370
+ setRunnerStatusError(err instanceof Error ? err.message : String(err));
371
+ }
372
+ } finally {
373
+ if (mountedRef.current) setRunnerStatusLoading(false);
374
+ }
375
+ }, []);
376
+
377
+ const loadRoutingPrefs = useCallback(async () => {
378
+ try {
379
+ const [runner, model] = await Promise.all([
380
+ getPreferredRunner(),
381
+ getPreferredModel(),
382
+ ]);
383
+ if (!mountedRef.current) return;
384
+ setPreferredRunnerState(runner);
385
+ setPreferredModelState(model ?? '');
386
+ } catch {
387
+ if (!mountedRef.current) return;
388
+ setPreferredRunnerState(null);
389
+ setPreferredModelState('');
390
+ }
391
+ }, []);
392
+
198
393
  useEffect(() => {
199
394
  mountedRef.current = true;
200
395
  const sub = DeviceEventEmitter.addListener('yaverFeedback:startReport', () => {
@@ -220,6 +415,7 @@ export const FeedbackModal: React.FC = () => {
220
415
  })
221
416
  .catch(() => {});
222
417
  void loadSelectedMachine();
418
+ void loadRunnerStatuses();
223
419
  }
224
420
  });
225
421
  // Agent streams build / compile progress through the BlackBox
@@ -248,15 +444,16 @@ export const FeedbackModal: React.FC = () => {
248
444
  sub.remove();
249
445
  statusSub.remove();
250
446
  };
251
- }, [loadSelectedMachine]);
447
+ }, [loadRunnerStatuses, loadSelectedMachine]);
252
448
 
253
449
  useEffect(() => {
254
450
  if (!visible) return;
255
451
  const interval = setInterval(() => {
256
452
  void loadSelectedMachine();
453
+ void loadRunnerStatuses();
257
454
  }, 5000);
258
455
  return () => clearInterval(interval);
259
- }, [loadSelectedMachine, visible]);
456
+ }, [loadRunnerStatuses, loadSelectedMachine, visible]);
260
457
 
261
458
  useEffect(() => {
262
459
  if (!visible) {
@@ -292,6 +489,7 @@ export const FeedbackModal: React.FC = () => {
292
489
  setAction('idle');
293
490
  setShowVibeInput(false);
294
491
  setVibePrompt('');
492
+ setRunnerStatusError(null);
295
493
  }, []);
296
494
 
297
495
  // Helper: run a P2P call; on network failure, ask YaverFeedback to
@@ -535,6 +733,20 @@ export const FeedbackModal: React.FC = () => {
535
733
  }
536
734
  }, [showVibeInput, vibePrompt]);
537
735
 
736
+ // Hold the active vibe-chat session — set when handleVibingSubmit
737
+ // returns a fresh taskId. Renders <VibeChatScreen> which streams the
738
+ // SSE transcript, supports multi-turn follow-ups via /tasks/{id}/
739
+ // resume, and exposes a Reload button. Mirrors the in-Yaver native
740
+ // pane's transcript-mode behaviour, just rendered in RN here.
741
+ const [activeVibe, setActiveVibe] = useState<{
742
+ taskId: string;
743
+ initialPrompt: string;
744
+ project?: string;
745
+ runner?: string;
746
+ model?: string;
747
+ } | null>(null);
748
+ const [includeScreenshot, setIncludeScreenshot] = useState<boolean>(true);
749
+
538
750
  const handleVibingSubmit = useCallback(async () => {
539
751
  const client = YaverFeedback.getP2PClient();
540
752
  if (!client) {
@@ -554,13 +766,53 @@ export const FeedbackModal: React.FC = () => {
554
766
  .join('\n')
555
767
  : '';
556
768
  const userPrompt = vibePrompt.trim();
557
- const prompt = userPrompt
769
+ const promptText = userPrompt
558
770
  ? userPrompt + errNote
559
771
  : 'Pick the next small improvement or fix for this app based on recent activity and the current screen.' +
560
772
  errNote;
561
- const result = await client.vibing(prompt);
773
+
774
+ // Optional screenshot — captured from the host app's window.
775
+ // captureScreenshotBase64 returns null when react-native-view-
776
+ // shot isn't installed; we skip the screenshot rather than
777
+ // abort the whole feedback in that case.
778
+ let screenshotBase64: string | undefined;
779
+ if (includeScreenshot) {
780
+ const cap = await import('./capture');
781
+ const captured = await cap.captureScreenshotBase64();
782
+ if (captured?.base64) {
783
+ screenshotBase64 = captured.base64;
784
+ }
785
+ }
786
+
787
+ // Resolve project context the same way reloadApp / vibing did.
788
+ const { resolveAppIdentity } = await import('./P2PClient');
789
+ const identity = resolveAppIdentity();
790
+
791
+ // Pull the user's preferred runner / model from local prefs.
792
+ // Both are optional — the agent falls back to whatever runner
793
+ // is signed in if neither is provided.
794
+ const prefs = await import('./preferences');
795
+ const preferredRunner = (await prefs.getPreferredRunner?.()) ?? null;
796
+ const preferredModel = (await prefs.getPreferredModel?.()) ?? null;
797
+
798
+ const result = await client.createFeedbackTask({
799
+ userPrompt: promptText,
800
+ projectName: identity.projectName,
801
+ projectPath: identity.projectPath,
802
+ runner: preferredRunner ?? undefined,
803
+ model: preferredModel ?? undefined,
804
+ screenshotBase64,
805
+ });
562
806
  setLastVibeTaskId(result.taskId);
563
- setToast(`Vibing task ${result.taskId.slice(0, 8)} created`);
807
+ // Hand off to VibeChatScreen — it streams the SSE transcript,
808
+ // accepts follow-ups, and surfaces a Reload button.
809
+ setActiveVibe({
810
+ taskId: result.taskId,
811
+ initialPrompt: promptText,
812
+ project: identity.projectName,
813
+ runner: preferredRunner ?? undefined,
814
+ model: preferredModel ?? undefined,
815
+ });
564
816
  setVibePrompt('');
565
817
  setShowVibeInput(false);
566
818
  } catch (err: unknown) {
@@ -568,7 +820,7 @@ export const FeedbackModal: React.FC = () => {
568
820
  } finally {
569
821
  if (mountedRef.current) setAction('idle');
570
822
  }
571
- }, [vibePrompt]);
823
+ }, [vibePrompt, includeScreenshot]);
572
824
 
573
825
  /*
574
826
  const handleScreenRecording = useCallback(async () => {
@@ -577,6 +829,48 @@ export const FeedbackModal: React.FC = () => {
577
829
  */
578
830
 
579
831
  const busy = action !== 'idle';
832
+ const readyRunnerCount = runnerCards.filter((row) => row.ready || row.authConfigured).length;
833
+ const missingRunnerCount = runnerCards.filter((row) => !row.installed).length;
834
+ const needsAuthRunnerCount = runnerCards.filter(
835
+ (row) => row.installed && !row.authConfigured && !row.ready,
836
+ ).length;
837
+
838
+ // Once the user fires off a vibe task, swap the entire modal body
839
+ // for the live chat screen. The chat manages its own SSE
840
+ // subscription, multi-turn follow-ups, and Reload button. Closing
841
+ // the chat returns to idle and clears the active vibe.
842
+ if (visible && activeVibe) {
843
+ const client = YaverFeedback.getP2PClient();
844
+ return (
845
+ <>
846
+ <AuthOverlay />
847
+ <QuickActionIcon />
848
+ <Modal
849
+ visible={visible}
850
+ animationType="slide"
851
+ transparent
852
+ onRequestClose={() => setActiveVibe(null)}
853
+ >
854
+ {client ? (
855
+ <VibeChatScreen
856
+ client={client}
857
+ initialTaskId={activeVibe.taskId}
858
+ initialUserPrompt={activeVibe.initialPrompt}
859
+ project={activeVibe.project}
860
+ runner={activeVibe.runner}
861
+ model={activeVibe.model}
862
+ onClose={() => setActiveVibe(null)}
863
+ onReload={async () => {
864
+ const c = YaverFeedback.getP2PClient();
865
+ if (!c) throw new Error('Not connected');
866
+ await c.reloadApp();
867
+ }}
868
+ />
869
+ ) : null}
870
+ </Modal>
871
+ </>
872
+ );
873
+ }
580
874
 
581
875
  return (
582
876
  <>
@@ -597,7 +891,21 @@ export const FeedbackModal: React.FC = () => {
597
891
  pointerEvents="box-none"
598
892
  >
599
893
  <Pressable
600
- style={styles.modal}
894
+ // Tablet: cap modal width and center as a card-style
895
+ // sheet rather than a phone bottom sheet that stretches
896
+ // across a 12.9" iPad. Phone behaviour unchanged.
897
+ style={[
898
+ styles.modal,
899
+ isTablet
900
+ ? {
901
+ width: '100%',
902
+ maxWidth: 640,
903
+ alignSelf: 'center',
904
+ borderTopLeftRadius: 22,
905
+ borderTopRightRadius: 22,
906
+ }
907
+ : null,
908
+ ]}
601
909
  onPress={(e) => {
602
910
  e.stopPropagation();
603
911
  Keyboard.dismiss();
@@ -664,6 +972,80 @@ export const FeedbackModal: React.FC = () => {
664
972
  <Text style={styles.machineMeta}>{machineCard.detail}</Text>
665
973
  </Pressable>
666
974
 
975
+ <View style={styles.runnerSection}>
976
+ <View style={styles.runnerSectionHeader}>
977
+ <View style={{ flex: 1 }}>
978
+ <Text style={styles.runnerSectionTitle}>Coding Agents</Text>
979
+ <Text style={styles.runnerSectionSubtitle}>
980
+ {runnerStatusLoading
981
+ ? 'Refreshing runner status on the selected machine…'
982
+ : `${readyRunnerCount} ready · ${needsAuthRunnerCount} need sign-in · ${missingRunnerCount} missing`}
983
+ </Text>
984
+ </View>
985
+ <Pressable
986
+ onPress={() => void loadRunnerStatuses()}
987
+ style={({ pressed }) => [
988
+ styles.runnerRefreshBtn,
989
+ pressed && styles.buttonPressed,
990
+ ]}
991
+ accessibilityRole="button"
992
+ accessibilityLabel="Refresh coding-agent status"
993
+ >
994
+ <Text style={styles.runnerRefreshBtnText}>
995
+ {runnerStatusLoading ? 'Refreshing…' : 'Refresh'}
996
+ </Text>
997
+ </Pressable>
998
+ </View>
999
+
1000
+ {runnerCards.map((row) => (
1001
+ <View
1002
+ key={row.id}
1003
+ style={[
1004
+ styles.runnerCard,
1005
+ row.tone === 'ok' && styles.runnerCardOk,
1006
+ row.tone === 'warning' && styles.runnerCardWarning,
1007
+ row.tone === 'error' && styles.runnerCardError,
1008
+ ]}
1009
+ >
1010
+ <View style={styles.runnerCardTop}>
1011
+ <View style={{ flex: 1 }}>
1012
+ <Text style={styles.runnerCardTitle}>{row.name}</Text>
1013
+ <Text
1014
+ style={[
1015
+ styles.runnerCardStatus,
1016
+ row.tone === 'ok' && styles.runnerCardStatusOk,
1017
+ row.tone === 'warning' && styles.runnerCardStatusWarning,
1018
+ row.tone === 'error' && styles.runnerCardStatusError,
1019
+ ]}
1020
+ >
1021
+ {row.statusLine}
1022
+ </Text>
1023
+ </View>
1024
+ {row.actionRunner ? (
1025
+ <Pressable
1026
+ onPress={() => setRunnerAuthModal(row.actionRunner ?? null)}
1027
+ style={({ pressed }) => [
1028
+ styles.runnerActionBtn,
1029
+ pressed && styles.buttonPressed,
1030
+ ]}
1031
+ accessibilityRole="button"
1032
+ accessibilityLabel={`${row.actionLabel} ${row.name}`}
1033
+ >
1034
+ <Text style={styles.runnerActionBtnText}>{row.actionLabel}</Text>
1035
+ </Pressable>
1036
+ ) : null}
1037
+ </View>
1038
+ {row.detail ? (
1039
+ <Text style={styles.runnerCardDetail}>{row.detail}</Text>
1040
+ ) : null}
1041
+ </View>
1042
+ ))}
1043
+
1044
+ {runnerStatusError ? (
1045
+ <Text style={styles.runnerSectionError}>{runnerStatusError}</Text>
1046
+ ) : null}
1047
+ </View>
1048
+
667
1049
  {quickIconHidden && (
668
1050
  <View style={styles.quickIconNote}>
669
1051
  <Text style={styles.quickIconNoteText}>
@@ -703,6 +1085,7 @@ export const FeedbackModal: React.FC = () => {
703
1085
  }}
704
1086
  style={[
705
1087
  styles.iconOption,
1088
+ iconOptionWidthOverride ? { width: iconOptionWidthOverride } : null,
706
1089
  selected && styles.iconOptionSelected,
707
1090
  ]}
708
1091
  >
@@ -811,40 +1194,22 @@ export const FeedbackModal: React.FC = () => {
811
1194
  busy={action === 'capturing'}
812
1195
  />
813
1196
 
814
- {/* Remote sign-in buttons trigger codex/claude device-auth
815
- on the selected agent without leaving the app. Opens a
816
- small native modal showing the verification URL + 8-char
817
- code the user enters in any browser. No API keys. */}
818
- <View style={runnerAuthRowStyles.container}>
819
- <Pressable
820
- onPress={() => setRunnerAuthModal('codex')}
821
- disabled={busy}
822
- style={({ pressed }) => [
823
- runnerAuthRowStyles.button,
824
- pressed && runnerAuthRowStyles.buttonPressed,
825
- busy && runnerAuthRowStyles.buttonDisabled,
826
- ]}
827
- accessibilityRole="button"
828
- accessibilityLabel="Remote sign-in Codex"
829
- >
830
- <Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
831
- <Text style={runnerAuthRowStyles.buttonName}>Codex</Text>
832
- </Pressable>
833
- <Pressable
834
- onPress={() => setRunnerAuthModal('claude')}
1197
+ {/* Deploy opens an inline panel that talks to
1198
+ /fleet/deploy-options on the agent and lets the user
1199
+ pick TestFlight / Play / Both, then a machine to run
1200
+ it on. Capabilities (e.g. "Linux can't TestFlight")
1201
+ come from the agent's doctor probes — no client-side
1202
+ platform smarts here. */}
1203
+ {!showDeploy ? (
1204
+ <ActionRow
1205
+ label="Deploy"
1206
+ tint="#7f8cf7"
1207
+ onPress={() => setShowDeploy(true)}
835
1208
  disabled={busy}
836
- style={({ pressed }) => [
837
- runnerAuthRowStyles.button,
838
- pressed && runnerAuthRowStyles.buttonPressed,
839
- busy && runnerAuthRowStyles.buttonDisabled,
840
- ]}
841
- accessibilityRole="button"
842
- accessibilityLabel="Remote sign-in Claude"
843
- >
844
- <Text style={runnerAuthRowStyles.buttonLabel}>Remote sign-in</Text>
845
- <Text style={runnerAuthRowStyles.buttonName}>Claude</Text>
846
- </Pressable>
847
- </View>
1209
+ />
1210
+ ) : (
1211
+ <DeployPanel onClose={() => setShowDeploy(false)} />
1212
+ )}
848
1213
 
849
1214
  {progress !== null && (
850
1215
  <View style={styles.progressTrack}>
@@ -879,7 +1244,10 @@ export const FeedbackModal: React.FC = () => {
879
1244
  {runnerAuthModal ? (
880
1245
  <RunnerAuthNativeModal
881
1246
  runner={runnerAuthModal}
882
- onClose={() => setRunnerAuthModal(null)}
1247
+ onClose={() => {
1248
+ setRunnerAuthModal(null);
1249
+ void loadRunnerStatuses();
1250
+ }}
883
1251
  />
884
1252
  ) : null}
885
1253
  </>
@@ -1107,6 +1475,106 @@ const styles = StyleSheet.create({
1107
1475
  marginTop: 4,
1108
1476
  lineHeight: 17,
1109
1477
  },
1478
+ runnerSection: {
1479
+ marginTop: 2,
1480
+ gap: 10,
1481
+ },
1482
+ runnerSectionHeader: {
1483
+ flexDirection: 'row',
1484
+ alignItems: 'center',
1485
+ gap: 10,
1486
+ },
1487
+ runnerSectionTitle: {
1488
+ color: '#f8fafc',
1489
+ fontSize: 16,
1490
+ fontWeight: '700',
1491
+ },
1492
+ runnerSectionSubtitle: {
1493
+ marginTop: 2,
1494
+ color: '#94a3b8',
1495
+ fontSize: 12,
1496
+ },
1497
+ runnerRefreshBtn: {
1498
+ borderRadius: 10,
1499
+ borderWidth: 1,
1500
+ borderColor: 'rgba(148,163,184,0.22)',
1501
+ backgroundColor: 'rgba(15,23,42,0.65)',
1502
+ paddingHorizontal: 10,
1503
+ paddingVertical: 8,
1504
+ },
1505
+ runnerRefreshBtnText: {
1506
+ color: '#cbd5e1',
1507
+ fontSize: 12,
1508
+ fontWeight: '600',
1509
+ },
1510
+ runnerCard: {
1511
+ borderRadius: 12,
1512
+ borderWidth: 1,
1513
+ borderColor: 'rgba(148,163,184,0.14)',
1514
+ backgroundColor: 'rgba(15,23,42,0.45)',
1515
+ paddingHorizontal: 12,
1516
+ paddingVertical: 11,
1517
+ gap: 6,
1518
+ },
1519
+ runnerCardOk: {
1520
+ borderColor: 'rgba(34,197,94,0.28)',
1521
+ backgroundColor: 'rgba(20,83,45,0.20)',
1522
+ },
1523
+ runnerCardWarning: {
1524
+ borderColor: 'rgba(251,191,36,0.28)',
1525
+ backgroundColor: 'rgba(120,53,15,0.18)',
1526
+ },
1527
+ runnerCardError: {
1528
+ borderColor: 'rgba(248,113,113,0.28)',
1529
+ backgroundColor: 'rgba(127,29,29,0.18)',
1530
+ },
1531
+ runnerCardTop: {
1532
+ flexDirection: 'row',
1533
+ alignItems: 'center',
1534
+ gap: 10,
1535
+ },
1536
+ runnerCardTitle: {
1537
+ color: '#f8fafc',
1538
+ fontSize: 14,
1539
+ fontWeight: '700',
1540
+ },
1541
+ runnerCardStatus: {
1542
+ marginTop: 2,
1543
+ fontSize: 12,
1544
+ color: '#cbd5e1',
1545
+ },
1546
+ runnerCardStatusOk: {
1547
+ color: '#86efac',
1548
+ },
1549
+ runnerCardStatusWarning: {
1550
+ color: '#fcd34d',
1551
+ },
1552
+ runnerCardStatusError: {
1553
+ color: '#fca5a5',
1554
+ },
1555
+ runnerCardDetail: {
1556
+ color: '#94a3b8',
1557
+ fontSize: 11,
1558
+ lineHeight: 16,
1559
+ },
1560
+ runnerActionBtn: {
1561
+ borderRadius: 10,
1562
+ borderWidth: 1,
1563
+ borderColor: 'rgba(129,140,248,0.35)',
1564
+ backgroundColor: 'rgba(67,56,202,0.22)',
1565
+ paddingHorizontal: 12,
1566
+ paddingVertical: 8,
1567
+ },
1568
+ runnerActionBtnText: {
1569
+ color: '#c7d2fe',
1570
+ fontSize: 12,
1571
+ fontWeight: '700',
1572
+ },
1573
+ runnerSectionError: {
1574
+ color: '#fca5a5',
1575
+ fontSize: 12,
1576
+ lineHeight: 18,
1577
+ },
1110
1578
  captureChoices: {
1111
1579
  gap: 10,
1112
1580
  },
@@ -1484,40 +1952,6 @@ const RunnerAuthNativeModal: React.FC<{
1484
1952
  );
1485
1953
  };
1486
1954
 
1487
- const runnerAuthRowStyles = StyleSheet.create({
1488
- container: {
1489
- flexDirection: 'row',
1490
- gap: 8,
1491
- marginTop: 8,
1492
- flexWrap: 'wrap',
1493
- },
1494
- button: {
1495
- flexGrow: 1,
1496
- flexBasis: 0,
1497
- minWidth: 120,
1498
- paddingHorizontal: 12,
1499
- paddingVertical: 10,
1500
- borderRadius: 10,
1501
- borderWidth: 1,
1502
- borderColor: 'rgba(148,163,184,0.22)',
1503
- backgroundColor: 'rgba(15,23,42,0.6)',
1504
- },
1505
- buttonPressed: { opacity: 0.7 },
1506
- buttonDisabled: { opacity: 0.4 },
1507
- buttonLabel: {
1508
- fontSize: 10,
1509
- color: '#94a3b8',
1510
- textTransform: 'uppercase',
1511
- letterSpacing: 0.8,
1512
- },
1513
- buttonName: {
1514
- marginTop: 2,
1515
- fontSize: 14,
1516
- fontWeight: '600',
1517
- color: '#f1f5f9',
1518
- },
1519
- });
1520
-
1521
1955
  const runnerAuthModalStyles = StyleSheet.create({
1522
1956
  overlay: {
1523
1957
  flex: 1,