yaver-feedback-react-native 0.9.2 → 0.9.3

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.
Files changed (103) hide show
  1. package/README.md +102 -1
  2. package/dist/AuthOverlay.d.ts +1 -14
  3. package/dist/AuthOverlay.js +9 -62
  4. package/dist/Discovery.js +0 -6
  5. package/dist/DogfoodRuntime.d.ts +124 -0
  6. package/dist/DogfoodRuntime.js +273 -0
  7. package/dist/FeedbackModal.js +347 -61
  8. package/dist/LoginScreen.d.ts +1 -5
  9. package/dist/LoginScreen.js +2 -6
  10. package/dist/MachinePickerScreen.d.ts +1 -3
  11. package/dist/MachinePickerScreen.js +6 -18
  12. package/dist/P2PClient.d.ts +116 -3
  13. package/dist/P2PClient.js +273 -3
  14. package/dist/P2PDogfoodDriver.d.ts +12 -0
  15. package/dist/P2PDogfoodDriver.js +118 -0
  16. package/dist/PairDeviceModal.d.ts +2 -3
  17. package/dist/VibeChatScreen.d.ts +11 -1
  18. package/dist/VibeChatScreen.js +200 -41
  19. package/dist/YaverFeedback.d.ts +34 -0
  20. package/dist/YaverFeedback.js +124 -19
  21. package/dist/YaverModeBadge.d.ts +22 -0
  22. package/dist/YaverModeBadge.js +219 -0
  23. package/dist/__tests__/AuthDevices.test.js +1 -48
  24. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  25. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  26. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  27. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  28. package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
  29. package/dist/__tests__/ReportIdentity.test.js +34 -22
  30. package/dist/__tests__/YaverFeedback.test.js +13 -3
  31. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  32. package/dist/__tests__/deviceDogfood.test.js +86 -0
  33. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  34. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  35. package/dist/_core/ansi.d.ts +117 -0
  36. package/dist/_core/ansi.js +468 -0
  37. package/dist/_core/ansi.test.d.ts +1 -0
  38. package/dist/_core/ansi.test.js +225 -0
  39. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  40. package/dist/_core/buildFeedbackPrompt.js +18 -72
  41. package/dist/_core/constants.d.ts +19 -6
  42. package/dist/_core/constants.js +20 -7
  43. package/dist/_core/device.d.ts +9 -23
  44. package/dist/_core/device.js +13 -28
  45. package/dist/_core/endpoints.d.ts +0 -7
  46. package/dist/_core/endpoints.js +0 -7
  47. package/dist/_core/index.d.ts +4 -0
  48. package/dist/_core/index.js +4 -0
  49. package/dist/_core/remoteless.d.ts +44 -0
  50. package/dist/_core/remoteless.js +75 -0
  51. package/dist/_core/trace.d.ts +47 -0
  52. package/dist/_core/trace.js +38 -0
  53. package/dist/_core/trace.test.d.ts +1 -0
  54. package/dist/_core/trace.test.js +60 -0
  55. package/dist/auth.d.ts +3 -60
  56. package/dist/auth.js +6 -88
  57. package/dist/deviceDogfood.d.ts +53 -0
  58. package/dist/deviceDogfood.js +137 -0
  59. package/dist/dogfoodPolicy.d.ts +25 -0
  60. package/dist/dogfoodPolicy.js +24 -0
  61. package/dist/index.d.ts +13 -4
  62. package/dist/index.js +24 -8
  63. package/dist/reloadActions.js +2 -2
  64. package/dist/types.d.ts +50 -7
  65. package/package.json +12 -3
  66. package/src/AuthOverlay.tsx +20 -106
  67. package/src/Discovery.ts +0 -6
  68. package/src/DogfoodRuntime.ts +373 -0
  69. package/src/FeedbackModal.tsx +445 -67
  70. package/src/LoginScreen.tsx +2 -22
  71. package/src/MachinePickerScreen.tsx +6 -21
  72. package/src/P2PClient.ts +347 -4
  73. package/src/P2PDogfoodDriver.ts +132 -0
  74. package/src/PairDeviceModal.tsx +2 -3
  75. package/src/VibeChatScreen.tsx +232 -42
  76. package/src/YaverFeedback.ts +133 -22
  77. package/src/YaverModeBadge.tsx +234 -0
  78. package/src/__tests__/AuthDevices.test.ts +1 -52
  79. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  80. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  81. package/src/__tests__/ReportIdentity.test.ts +36 -27
  82. package/src/__tests__/YaverFeedback.test.ts +15 -3
  83. package/src/__tests__/deviceDogfood.test.ts +77 -0
  84. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  85. package/src/_core/ansi.test.ts +250 -0
  86. package/src/_core/ansi.ts +475 -0
  87. package/src/_core/buildFeedbackPrompt.ts +18 -95
  88. package/src/_core/constants.ts +20 -6
  89. package/src/_core/device.ts +13 -30
  90. package/src/_core/endpoints.ts +0 -7
  91. package/src/_core/index.ts +4 -0
  92. package/src/_core/remoteless.ts +110 -0
  93. package/src/_core/trace.test.ts +58 -0
  94. package/src/_core/trace.ts +75 -0
  95. package/src/auth.ts +7 -156
  96. package/src/deviceDogfood.ts +171 -0
  97. package/src/dogfoodPolicy.ts +40 -0
  98. package/src/index.ts +40 -11
  99. package/src/reloadActions.ts +2 -2
  100. package/src/types.ts +45 -7
  101. package/dist/GuestOnboardingScreen.d.ts +0 -8
  102. package/dist/GuestOnboardingScreen.js +0 -282
  103. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -19,6 +19,9 @@
19
19
  import React, { useCallback, useEffect, useRef, useState } from 'react';
20
20
  import {
21
21
  ActivityIndicator,
22
+ Alert,
23
+ KeyboardAvoidingView,
24
+ Platform,
22
25
  ScrollView,
23
26
  StyleSheet,
24
27
  Text,
@@ -52,8 +55,24 @@ interface Props {
52
55
  /** Optional context forwarded to the voice stream so the agent runs
53
56
  * the task against the right project / runner / model. */
54
57
  project?: string;
58
+ projectPath?: string;
55
59
  model?: string;
56
60
  runner?: string;
61
+ /** Show the optional voice/STT controls. Defaults to keyboard-only. */
62
+ voiceInputEnabled?: boolean;
63
+ /** Standalone SDK hosts use this to fold back to the floating Y without
64
+ * destroying the live task subscription or transcript state. */
65
+ onMinimize?: () => void;
66
+ codingMachine?: string;
67
+ renderMachine?: string;
68
+ /** Return to the SDK prompt composer to create a separate task/topic. */
69
+ onNewTopic?: () => void;
70
+ }
71
+
72
+ interface VibeThreadCard {
73
+ id: string;
74
+ title: string;
75
+ status: string;
57
76
  }
58
77
 
59
78
  export function VibeChatScreen({
@@ -63,9 +82,16 @@ export function VibeChatScreen({
63
82
  onClose,
64
83
  onReload,
65
84
  project,
85
+ projectPath,
66
86
  model,
67
87
  runner,
88
+ voiceInputEnabled = false,
89
+ onMinimize,
90
+ codingMachine,
91
+ renderMachine,
92
+ onNewTopic,
68
93
  }: Props) {
94
+ const [activeTab, setActiveTab] = useState<'chat' | 'settings'>('chat');
69
95
  const [taskId, setTaskId] = useState(initialTaskId);
70
96
  const [turns, setTurns] = useState<VibeTurn[]>(() => [
71
97
  {
@@ -82,13 +108,29 @@ export function VibeChatScreen({
82
108
  },
83
109
  ]);
84
110
  const [streamBuffer, setStreamBuffer] = useState('');
111
+ const [streamEpoch, setStreamEpoch] = useState(0);
85
112
  const [status, setStatus] = useState<'running' | 'done' | 'failed'>('running');
86
113
  const [followUp, setFollowUp] = useState('');
87
114
  const [isResuming, setIsResuming] = useState(false);
88
115
  const [isReloading, setIsReloading] = useState(false);
116
+ const [reloadQueued, setReloadQueued] = useState(false);
117
+ const [threads, setThreads] = useState<VibeThreadCard[]>(() => [{
118
+ id: initialTaskId,
119
+ title: initialUserPrompt || 'New topic',
120
+ status: 'running',
121
+ }]);
89
122
  const scrollRef = useRef<ScrollView | null>(null);
90
123
  const abortRef = useRef<(() => void) | null>(null);
91
124
 
125
+ const refreshThreads = useCallback(async () => {
126
+ try {
127
+ const list = await client.listVibeThreads({ projectName: project, projectPath });
128
+ setThreads(list.slice(0, 12));
129
+ } catch { /* history is advisory; keep the active conversation usable */ }
130
+ }, [client, project, projectPath]);
131
+
132
+ useEffect(() => { void refreshThreads(); }, [refreshThreads]);
133
+
92
134
  // ── Voice vibe coding ──────────────────────────────────────────────
93
135
  const [voiceState, setVoiceState] = useState<VoiceState>('idle');
94
136
  const [voiceAvailable, setVoiceAvailable] = useState(false);
@@ -110,6 +152,7 @@ export function VibeChatScreen({
110
152
  taskId,
111
153
  (line) => {
112
154
  if (!live) return;
155
+ if (line.includes('YAVER_THREAD_TITLE')) return;
113
156
  // Filter our internal error sentinel from the SSE helper.
114
157
  if (line.startsWith('__error__:')) {
115
158
  setStatus('failed');
@@ -122,7 +165,7 @@ export function VibeChatScreen({
122
165
  },
123
166
  (terminal) => {
124
167
  if (!live) return;
125
- setStatus(terminal === 'completed' ? 'done' : 'failed');
168
+ setStatus(terminal === 'completed' || terminal === 'review' ? 'done' : 'failed');
126
169
  // Move the buffered stream into a real assistant turn so the
127
170
  // user sees a stable render and can scroll back, then clear
128
171
  // the buffer for any follow-up.
@@ -139,6 +182,8 @@ export function VibeChatScreen({
139
182
  return next;
140
183
  });
141
184
  setStreamBuffer('');
185
+ setThreads((prev) => prev.map((thread) => thread.id === taskId ? { ...thread, status: terminal } : thread));
186
+ void refreshThreads();
142
187
  },
143
188
  );
144
189
  abortRef.current = close;
@@ -146,7 +191,45 @@ export function VibeChatScreen({
146
191
  live = false;
147
192
  try { close(); } catch { /* ignore */ }
148
193
  };
149
- }, [client, taskId]);
194
+ }, [client, refreshThreads, streamEpoch, taskId]);
195
+
196
+ const selectThread = useCallback(async (thread: VibeThreadCard) => {
197
+ abortRef.current?.();
198
+ try {
199
+ const task = await client.getVibeThread(thread.id);
200
+ setTaskId(task.id);
201
+ setStatus(task.status === 'running' || task.status === 'queued' ? 'running' : task.status === 'completed' || task.status === 'review' ? 'done' : 'failed');
202
+ setStreamBuffer('');
203
+ setTurns((task.turns || []).filter((turn) => turn.role === 'user' || turn.role === 'assistant').map((turn, index) => ({
204
+ id: `${task.id}-${index}`,
205
+ role: turn.role,
206
+ text: turn.content,
207
+ timestamp: turn.timestamp ? new Date(turn.timestamp).getTime() : Date.now() + index,
208
+ })));
209
+ } catch (error) {
210
+ setTurns((prev) => [...prev, { id: `thread-error-${Date.now()}`, role: 'status', text: error instanceof Error ? error.message : 'Could not open topic', timestamp: Date.now() }]);
211
+ }
212
+ }, [client]);
213
+
214
+ const removeThread = useCallback((thread: VibeThreadCard) => {
215
+ const message = thread.status === 'running' || thread.status === 'queued'
216
+ ? 'This also stops the coding turn that is still running.'
217
+ : 'This removes the conversation from your history.';
218
+ Alert.alert('Remove topic?', message, [
219
+ { text: 'Cancel', style: 'cancel' },
220
+ { text: 'Remove', style: 'destructive', onPress: () => {
221
+ void client.deleteVibeThread(thread.id).then(() => {
222
+ setThreads((prev) => prev.filter((item) => item.id !== thread.id));
223
+ if (thread.id === taskId) onNewTopic?.();
224
+ }).catch((error) => {
225
+ setTurns((prev) => [...prev, { id: `delete-error-${Date.now()}`, role: 'status', text: error instanceof Error ? error.message : 'Could not remove topic', timestamp: Date.now() }]);
226
+ });
227
+ } },
228
+ ]);
229
+ }, [client, onNewTopic, taskId]);
230
+
231
+ const runningThread = threads.find((thread) => thread.status === 'running' || thread.status === 'queued');
232
+ const codingLocked = status === 'running' || !!runningThread;
150
233
 
151
234
  // Auto-scroll the transcript when new content lands.
152
235
  useEffect(() => {
@@ -158,7 +241,7 @@ export function VibeChatScreen({
158
241
 
159
242
  const handleSendFollowUp = useCallback(async () => {
160
243
  const text = followUp.trim();
161
- if (!text || isResuming) return;
244
+ if (!text || isResuming || codingLocked) return;
162
245
  setIsResuming(true);
163
246
  // Add user turn immediately for snappy UX.
164
247
  setTurns((prev) => [
@@ -168,17 +251,13 @@ export function VibeChatScreen({
168
251
  ]);
169
252
  setFollowUp('');
170
253
  setStatus('running');
254
+ setThreads((prev) => prev.map((thread) => thread.id === taskId ? { ...thread, status: 'running' } : thread));
171
255
  setStreamBuffer('');
172
256
  try {
173
257
  await client.resumeTask({ taskId, userPrompt: text });
174
- // resumeTask reuses the same taskId, so the SSE subscription
175
- // above will pick up the new output stream automatically. To
176
- // force a fresh subscription we momentarily flip taskId to a
177
- // sentinel and back; cleaner than tearing down + re-attaching
178
- // the SSE manually.
179
- const same = taskId;
180
- setTaskId(`${same}#`);
181
- setTimeout(() => setTaskId(same), 0);
258
+ // resumeTask reuses the same task id; advance the subscription epoch so
259
+ // the existing SSE path reconnects without inventing an invalid id.
260
+ setStreamEpoch((value) => value + 1);
182
261
  } catch (e) {
183
262
  setStatus('failed');
184
263
  setTurns((prev) => [
@@ -193,10 +272,14 @@ export function VibeChatScreen({
193
272
  } finally {
194
273
  setIsResuming(false);
195
274
  }
196
- }, [client, followUp, isResuming, taskId]);
275
+ }, [client, codingLocked, followUp, isResuming, taskId]);
197
276
 
198
277
  const handleReload = useCallback(async () => {
199
278
  if (isReloading || !onReload) return;
279
+ if (status === 'running') {
280
+ setReloadQueued(true);
281
+ return;
282
+ }
200
283
  setIsReloading(true);
201
284
  try {
202
285
  await onReload();
@@ -213,7 +296,13 @@ export function VibeChatScreen({
213
296
  } finally {
214
297
  setIsReloading(false);
215
298
  }
216
- }, [isReloading, onReload]);
299
+ }, [isReloading, onReload, status]);
300
+
301
+ useEffect(() => {
302
+ if (!reloadQueued || status === 'running' || isReloading) return;
303
+ setReloadQueued(false);
304
+ void handleReload();
305
+ }, [handleReload, isReloading, reloadQueued, status]);
217
306
 
218
307
  // Probe whether voice is usable: deps present (expo-av + expo-file-
219
308
  // system + buffer) AND the agent reports STT/TTS ready. Hide the mic
@@ -221,6 +310,7 @@ export function VibeChatScreen({
221
310
  useEffect(() => {
222
311
  let cancelled = false;
223
312
  (async () => {
313
+ if (!voiceInputEnabled) return;
224
314
  if (!isVoiceCaptureSupported() || !isVoiceStreamSupported()) return;
225
315
  try {
226
316
  const res = await fetch(`${client.agentBaseUrl}/voice/status`, { headers: client.voiceAuthHeaders() });
@@ -237,7 +327,7 @@ export function VibeChatScreen({
237
327
  } catch { /* leave hidden */ }
238
328
  })();
239
329
  return () => { cancelled = true; };
240
- }, [client]);
330
+ }, [client, voiceInputEnabled]);
241
331
 
242
332
  useEffect(() => () => { voiceSessionRef.current?.close(); }, []);
243
333
 
@@ -367,19 +457,60 @@ export function VibeChatScreen({
367
457
  };
368
458
 
369
459
  return (
370
- <View style={styles.container}>
460
+ <KeyboardAvoidingView
461
+ style={styles.container}
462
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
463
+ keyboardVerticalOffset={0}
464
+ >
371
465
  <View style={styles.header}>
372
- <Text style={styles.title}>Vibe</Text>
373
- {onClose && (
374
- <TouchableOpacity onPress={onClose} accessibilityLabel="Close vibe chat">
375
- <Text style={styles.close}>✕</Text>
466
+ <View>
467
+ <Text style={styles.title}>Vibing</Text>
468
+ <Text style={styles.routeCaption} numberOfLines={1}>
469
+ {[runner || 'automatic runner', model].filter(Boolean).join(' · ')}
470
+ </Text>
471
+ </View>
472
+ <View style={styles.headerActions}>
473
+ {onMinimize ? <TouchableOpacity onPress={onMinimize} accessibilityLabel="Minimize Vibing"><Text style={styles.close}>−</Text></TouchableOpacity> : null}
474
+ {onClose ? <TouchableOpacity onPress={onClose} accessibilityLabel="End vibe chat"><Text style={styles.close}>✕</Text></TouchableOpacity> : null}
475
+ </View>
476
+ </View>
477
+
478
+ <View style={styles.tabs} accessibilityRole="tablist">
479
+ {(['chat', 'settings'] as const).map((tab) => (
480
+ <TouchableOpacity key={tab} onPress={() => setActiveTab(tab)} style={[styles.tab, activeTab === tab && styles.tabSelected]} accessibilityRole="tab" accessibilityState={{ selected: activeTab === tab }}>
481
+ <Text style={[styles.tabText, activeTab === tab && styles.tabTextSelected]}>{tab === 'chat' ? 'Chat' : 'Settings'}</Text>
376
482
  </TouchableOpacity>
377
- )}
483
+ ))}
378
484
  </View>
379
485
 
486
+ {activeTab === 'chat' && threads.length > 1 ? <View style={styles.topicRailWrap}>
487
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.topicRail}>
488
+ <TouchableOpacity style={styles.newTopicCard} onPress={onNewTopic} accessibilityLabel="Start a new topic">
489
+ <Text style={styles.newTopicPlus}>+</Text>
490
+ <Text style={styles.newTopicText}>New</Text>
491
+ </TouchableOpacity>
492
+ {threads.map((thread) => (
493
+ <TouchableOpacity key={thread.id} style={[styles.topicCard, thread.id === taskId && styles.topicCardSelected]} onPress={() => { void selectThread(thread); }} accessibilityLabel={`Open ${thread.title}`}>
494
+ <View style={styles.topicCardTopline}>
495
+ <View style={[styles.topicDot, (thread.status === 'running' || thread.status === 'queued') && styles.topicDotLive]} />
496
+ <Text style={styles.topicStatus}>{thread.status === 'completed' ? 'done' : thread.status}</Text>
497
+ <TouchableOpacity onPress={() => removeThread(thread)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} accessibilityLabel={`Remove ${thread.title}`}><Text style={styles.topicRemove}>×</Text></TouchableOpacity>
498
+ </View>
499
+ <Text style={styles.topicTitle} numberOfLines={2}>{thread.title}</Text>
500
+ </TouchableOpacity>
501
+ ))}
502
+ </ScrollView>
503
+ </View> : activeTab === 'chat' && threads.length === 1 ? <View style={styles.singleTopicBar}>
504
+ <TouchableOpacity style={styles.singleTopicTitleButton} onPress={() => { void selectThread(threads[0]); }} accessibilityLabel={`Open ${threads[0].title}`}>
505
+ <Text style={styles.singleTopicTitle} numberOfLines={1}>{threads[0].title || 'Current topic'}</Text>
506
+ </TouchableOpacity>
507
+ {onNewTopic ? <TouchableOpacity style={styles.singleTopicAction} onPress={onNewTopic} accessibilityLabel="Start a new topic"><Text style={styles.singleTopicActionText}>+</Text></TouchableOpacity> : null}
508
+ <TouchableOpacity style={styles.singleTopicAction} onPress={() => removeThread(threads[0])} accessibilityLabel={`Remove ${threads[0].title}`}><Text style={styles.singleTopicRemove}>×</Text></TouchableOpacity>
509
+ </View> : null}
510
+
380
511
  <ScrollView
381
512
  ref={scrollRef}
382
- style={styles.transcript}
513
+ style={[styles.transcript, activeTab !== 'chat' && styles.hidden]}
383
514
  contentContainerStyle={styles.transcriptContent}
384
515
  keyboardShouldPersistTaps="handled"
385
516
  >
@@ -413,8 +544,28 @@ export function VibeChatScreen({
413
544
  )}
414
545
  </ScrollView>
415
546
 
416
- <View style={styles.footer}>
417
- {voiceAvailable && voiceState !== 'idle' && (
547
+ <ScrollView style={[styles.settings, activeTab !== 'settings' && styles.hidden]} contentContainerStyle={styles.settingsContent}>
548
+ <Text style={styles.settingsLabel}>Coding route</Text>
549
+ <View style={styles.settingsCard}>
550
+ <Text style={styles.settingsMeta}>Coding machine · {codingMachine || 'selected machine'}</Text>
551
+ <Text style={styles.settingsMeta}>Render machine · {renderMachine || codingMachine || 'same machine'}</Text>
552
+ <Text style={styles.settingsTitle}>{runner || 'Automatic runner'}</Text>
553
+ <Text style={styles.settingsMeta}>{model || 'Runner default model'}</Text>
554
+ {project ? <Text style={styles.settingsMeta}>{project}</Text> : null}
555
+ </View>
556
+ <Text style={styles.settingsLabel}>Preview</Text>
557
+ <View style={styles.settingsCard}>
558
+ <Text style={styles.settingsTitle}>Reload rendered app</Text>
559
+ <Text style={styles.settingsMeta}>{reloadQueued ? 'Reload queued. It will run when coding finishes.' : status === 'running' ? 'Reload will run when the current coding turn finishes.' : 'Apply the latest rendered output without ending this chat.'}</Text>
560
+ {onReload ? <TouchableOpacity style={[styles.settingsButton, isReloading && styles.actionBtnDisabled]} onPress={handleReload} disabled={isReloading}>
561
+ <Text style={styles.settingsButtonText}>{isReloading ? 'Reloading…' : reloadQueued ? 'Reload Queued' : status === 'running' ? 'Queue Reload' : 'Reload'}</Text>
562
+ </TouchableOpacity> : null}
563
+ </View>
564
+ {onMinimize ? <TouchableOpacity style={styles.returnButton} onPress={onMinimize} accessibilityLabel="Return to app and keep Vibing running"><Text style={styles.returnButtonText}>Return to App</Text></TouchableOpacity> : null}
565
+ </ScrollView>
566
+
567
+ <View style={[styles.footer, activeTab !== 'chat' && styles.hidden]}>
568
+ {voiceInputEnabled && voiceAvailable && voiceState !== 'idle' && (
418
569
  <Text style={styles.engineCaption}>
419
570
  {voiceState === 'recording' ? 'listening' : voiceState === 'uploading' ? 'sending' : voiceState === 'thinking' ? 'agent working' : 'speaking'}
420
571
  {activeEngine ? ` · ${activeEngine}` : ` · ${voiceMode === 'flux' ? 'Flux (Deepgram)' : 'Local (whisper)'}`}
@@ -424,13 +575,13 @@ export function VibeChatScreen({
424
575
  style={styles.input}
425
576
  value={followUp}
426
577
  onChangeText={setFollowUp}
427
- placeholder={status === 'running' ? 'wait for the agent…' : 'follow up…'}
578
+ placeholder={codingLocked ? runningThread?.id !== taskId ? 'another topic is coding…' : 'wait for the agent…' : 'follow up…'}
428
579
  placeholderTextColor="#666"
429
- editable={status !== 'running' && !isResuming}
580
+ editable={!codingLocked && !isResuming}
430
581
  multiline
431
582
  />
432
583
  <View style={styles.actions}>
433
- {voiceAvailable && (
584
+ {voiceInputEnabled && voiceAvailable && (
434
585
  <>
435
586
  {/* Local ↔ Flux engine toggle. Only shows Flux when the
436
587
  agent has a Deepgram key; otherwise the label just
@@ -483,10 +634,10 @@ export function VibeChatScreen({
483
634
  style={[
484
635
  styles.actionBtn,
485
636
  styles.sendBtn,
486
- (isResuming || status === 'running' || !followUp.trim()) && styles.actionBtnDisabled,
637
+ (isResuming || codingLocked || !followUp.trim()) && styles.actionBtnDisabled,
487
638
  ]}
488
639
  onPress={handleSendFollowUp}
489
- disabled={isResuming || status === 'running' || !followUp.trim()}
640
+ disabled={isResuming || codingLocked || !followUp.trim()}
490
641
  >
491
642
  <Text style={styles.actionText}>
492
643
  {isResuming ? '…' : '↑ send'}
@@ -494,12 +645,12 @@ export function VibeChatScreen({
494
645
  </TouchableOpacity>
495
646
  </View>
496
647
  </View>
497
- </View>
648
+ </KeyboardAvoidingView>
498
649
  );
499
650
  }
500
651
 
501
652
  const styles = StyleSheet.create({
502
- container: { flex: 1, backgroundColor: '#0a0a0a' },
653
+ container: { flex: 1, backgroundColor: '#f8f8fb' },
503
654
  header: {
504
655
  flexDirection: 'row',
505
656
  alignItems: 'center',
@@ -508,10 +659,36 @@ const styles = StyleSheet.create({
508
659
  paddingTop: 14,
509
660
  paddingBottom: 8,
510
661
  borderBottomWidth: 1,
511
- borderBottomColor: 'rgba(255,255,255,0.08)',
662
+ borderBottomColor: '#e5e5ec',
512
663
  },
513
- title: { color: '#fff', fontSize: 17, fontWeight: '600' },
514
- close: { color: '#9ca3af', fontSize: 18 },
664
+ title: { color: '#17171d', fontSize: 17, fontWeight: '700' },
665
+ routeCaption: { color: '#858590', fontSize: 10, marginTop: 2, maxWidth: 250 },
666
+ headerActions: { flexDirection: 'row', alignItems: 'center', gap: 22 },
667
+ close: { color: '#656570', fontSize: 20, fontWeight: '700' },
668
+ tabs: { flexDirection: 'row', gap: 6, marginHorizontal: 12, marginTop: 8, padding: 3, borderRadius: 12, backgroundColor: '#ededf3' },
669
+ tab: { flex: 1, minHeight: 34, borderRadius: 9, alignItems: 'center', justifyContent: 'center' },
670
+ tabSelected: { backgroundColor: '#fff' },
671
+ tabText: { color: '#858590', fontSize: 12, fontWeight: '700' },
672
+ tabTextSelected: { color: '#6252e8' },
673
+ topicRailWrap: { minHeight: 102, borderBottomWidth: 1, borderBottomColor: '#e5e5ec' },
674
+ topicRail: { paddingHorizontal: 12, paddingVertical: 10, gap: 9 },
675
+ singleTopicBar: { minHeight: 40, paddingLeft: 12, paddingRight: 8, flexDirection: 'row', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#e5e5ec' },
676
+ singleTopicTitleButton: { flex: 1, minWidth: 0, minHeight: 38, justifyContent: 'center' },
677
+ singleTopicTitle: { color: '#656570', fontSize: 11, fontWeight: '700' },
678
+ singleTopicAction: { width: 34, height: 34, alignItems: 'center', justifyContent: 'center' },
679
+ singleTopicActionText: { color: '#6252e8', fontSize: 20, lineHeight: 22 },
680
+ singleTopicRemove: { color: '#9a9aa5', fontSize: 20, lineHeight: 22 },
681
+ newTopicCard: { width: 72, minHeight: 80, borderRadius: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ebe8ff', borderWidth: 1, borderColor: '#d9d3ff' },
682
+ newTopicPlus: { color: '#6252e8', fontSize: 22, lineHeight: 24 },
683
+ newTopicText: { color: '#6252e8', fontSize: 12, fontWeight: '800' },
684
+ topicCard: { width: 172, minHeight: 80, borderRadius: 14, padding: 10, gap: 7, backgroundColor: '#fff', borderWidth: 1.5, borderColor: '#e1e1e8' },
685
+ topicCardSelected: { borderColor: '#7568f8', backgroundColor: '#faf9ff' },
686
+ topicCardTopline: { flexDirection: 'row', alignItems: 'center', gap: 6 },
687
+ topicDot: { width: 7, height: 7, borderRadius: 4, backgroundColor: '#9ca3af' },
688
+ topicDotLive: { backgroundColor: '#f59e0b' },
689
+ topicStatus: { flex: 1, color: '#858590', fontSize: 10, fontWeight: '700', textTransform: 'uppercase' },
690
+ topicRemove: { color: '#9a9aa5', fontSize: 18, lineHeight: 18 },
691
+ topicTitle: { color: '#24242b', fontSize: 13, lineHeight: 17, fontWeight: '700' },
515
692
  transcript: { flex: 1 },
516
693
  transcriptContent: { padding: 12, paddingBottom: 24 },
517
694
  turn: {
@@ -525,8 +702,8 @@ const styles = StyleSheet.create({
525
702
  alignSelf: 'flex-end',
526
703
  },
527
704
  turnAssistant: {
528
- backgroundColor: 'rgba(255,255,255,0.06)',
529
- borderColor: 'rgba(255,255,255,0.10)',
705
+ backgroundColor: '#fff',
706
+ borderColor: '#e4e4ea',
530
707
  borderWidth: 1,
531
708
  alignSelf: 'flex-start',
532
709
  },
@@ -535,7 +712,7 @@ const styles = StyleSheet.create({
535
712
  alignSelf: 'flex-start',
536
713
  paddingHorizontal: 4,
537
714
  },
538
- turnText: { color: '#f1f5f9', fontSize: 14, lineHeight: 20 },
715
+ turnText: { color: '#28282f', fontSize: 14, lineHeight: 20 },
539
716
  spinnerRow: {
540
717
  flexDirection: 'row',
541
718
  alignItems: 'center',
@@ -545,15 +722,17 @@ const styles = StyleSheet.create({
545
722
  spinnerText: { color: '#9ca3af', fontSize: 12, marginLeft: 8 },
546
723
  footer: {
547
724
  borderTopWidth: 1,
548
- borderTopColor: 'rgba(255,255,255,0.08)',
725
+ borderTopColor: '#e5e5ec',
549
726
  padding: 10,
550
727
  },
551
728
  input: {
552
729
  minHeight: 40,
553
730
  maxHeight: 120,
554
- color: '#f1f5f9',
731
+ color: '#28282f',
555
732
  fontSize: 14,
556
- backgroundColor: 'rgba(255,255,255,0.04)',
733
+ backgroundColor: '#fff',
734
+ borderWidth: 1,
735
+ borderColor: '#e2e2e9',
557
736
  borderRadius: 10,
558
737
  paddingHorizontal: 12,
559
738
  paddingVertical: 8,
@@ -570,12 +749,23 @@ const styles = StyleSheet.create({
570
749
  marginLeft: 8,
571
750
  },
572
751
  actionBtnDisabled: { opacity: 0.5 },
573
- reloadBtn: { backgroundColor: 'rgba(255,255,255,0.08)' },
752
+ reloadBtn: { backgroundColor: '#e9e9ef' },
574
753
  voiceBtn: { backgroundColor: 'rgba(16,185,129,0.18)' },
575
754
  voiceBtnActive: { backgroundColor: '#ef4444' },
576
- engineToggle: { backgroundColor: 'rgba(255,255,255,0.06)', marginRight: 'auto', marginLeft: 0 },
577
- engineToggleText: { color: '#cbd5e1', fontSize: 12, fontWeight: '600' },
755
+ engineToggle: { backgroundColor: '#e9e9ef', marginRight: 'auto', marginLeft: 0 },
756
+ engineToggleText: { color: '#555561', fontSize: 12, fontWeight: '600' },
578
757
  engineCaption: { color: '#9ca3af', fontSize: 11, marginBottom: 6, marginLeft: 4 },
579
758
  sendBtn: { backgroundColor: '#7582f5' },
580
759
  actionText: { color: '#fff', fontSize: 13, fontWeight: '600' },
760
+ settings: { flex: 1 },
761
+ settingsContent: { padding: 14, gap: 10, paddingBottom: 30 },
762
+ settingsLabel: { color: '#777782', fontSize: 11, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 4 },
763
+ settingsCard: { borderRadius: 14, padding: 14, gap: 5, backgroundColor: '#fff', borderWidth: 1, borderColor: '#e3e3e9' },
764
+ settingsTitle: { color: '#222229', fontSize: 14, fontWeight: '800' },
765
+ settingsMeta: { color: '#777782', fontSize: 12, lineHeight: 17 },
766
+ settingsButton: { alignSelf: 'flex-start', marginTop: 8, borderRadius: 10, backgroundColor: '#6f58f5', paddingHorizontal: 14, paddingVertical: 9 },
767
+ settingsButtonText: { color: '#fff', fontSize: 12, fontWeight: '800' },
768
+ returnButton: { minHeight: 46, borderRadius: 13, alignItems: 'center', justifyContent: 'center', backgroundColor: '#6f58f5', marginTop: 4 },
769
+ returnButtonText: { color: '#fff', fontWeight: '800' },
770
+ hidden: { display: 'none' },
581
771
  });