yaver-feedback-react-native 0.9.2 → 0.9.4
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.
- package/README.md +102 -1
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +116 -3
- package/dist/P2PClient.js +273 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/ShakeDetector.js +31 -17
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- package/dist/__tests__/ShakeDetectorWeb.test.d.ts +1 -0
- package/dist/__tests__/ShakeDetectorWeb.test.js +27 -0
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +9 -23
- package/dist/_core/device.js +13 -28
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/ShakeDetector.ts +31 -18
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- package/src/__tests__/ShakeDetectorWeb.test.ts +30 -0
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +13 -30
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
package/src/ShakeDetector.ts
CHANGED
|
@@ -96,6 +96,13 @@ export class ShakeDetector {
|
|
|
96
96
|
* want the extra native surface get the Dev-menu path only.
|
|
97
97
|
*/
|
|
98
98
|
private subscribeAccelerometer(onShake: () => void): void {
|
|
99
|
+
// expo-sensors exposes an Accelerometer facade on web, but browsers have
|
|
100
|
+
// no native shake gesture and some versions construct a NativeEventEmitter
|
|
101
|
+
// backed by a module without addListener/removeListeners. Subscribing then
|
|
102
|
+
// crashes the host app's React commit phase. Preserve the SDK's explicit
|
|
103
|
+
// controls on web and skip only this impossible sensor transport.
|
|
104
|
+
if (Platform.OS === 'web') return;
|
|
105
|
+
|
|
99
106
|
let Accelerometer: {
|
|
100
107
|
setUpdateInterval: (ms: number) => void;
|
|
101
108
|
addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
|
|
@@ -116,24 +123,30 @@ export class ShakeDetector {
|
|
|
116
123
|
// Some platforms reject zero-value intervals; fall through with defaults
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
try {
|
|
127
|
+
this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
|
|
128
|
+
// Magnitude of acceleration vector (in g). Subtract 1 so a stationary
|
|
129
|
+
// device reports ~0 rather than the 1g of gravity.
|
|
130
|
+
const mag = Math.sqrt(x * x + y * y + z * z);
|
|
131
|
+
if (mag - 1 < ACCEL_THRESHOLD_G - 1) return;
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
this.peakTimestamps.push(now);
|
|
135
|
+
// Drop peaks that fell out of the rolling window.
|
|
136
|
+
while (
|
|
137
|
+
this.peakTimestamps.length > 0 &&
|
|
138
|
+
now - this.peakTimestamps[0] > ACCEL_WINDOW_MS
|
|
139
|
+
) {
|
|
140
|
+
this.peakTimestamps.shift();
|
|
141
|
+
}
|
|
142
|
+
if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
|
|
143
|
+
this.fire(onShake);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
// A host may include expo-sensors without linking its native module.
|
|
148
|
+
// Optional shake support must never take down the host application.
|
|
149
|
+
this.accelSub = null;
|
|
150
|
+
}
|
|
138
151
|
}
|
|
139
152
|
}
|
package/src/VibeChatScreen.tsx
CHANGED
|
@@ -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
|
|
175
|
-
//
|
|
176
|
-
|
|
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
|
-
<
|
|
460
|
+
<KeyboardAvoidingView
|
|
461
|
+
style={styles.container}
|
|
462
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
463
|
+
keyboardVerticalOffset={0}
|
|
464
|
+
>
|
|
371
465
|
<View style={styles.header}>
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
<
|
|
375
|
-
|
|
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
|
-
<
|
|
417
|
-
{
|
|
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={
|
|
578
|
+
placeholder={codingLocked ? runningThread?.id !== taskId ? 'another topic is coding…' : 'wait for the agent…' : 'follow up…'}
|
|
428
579
|
placeholderTextColor="#666"
|
|
429
|
-
editable={
|
|
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 ||
|
|
637
|
+
(isResuming || codingLocked || !followUp.trim()) && styles.actionBtnDisabled,
|
|
487
638
|
]}
|
|
488
639
|
onPress={handleSendFollowUp}
|
|
489
|
-
disabled={isResuming ||
|
|
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
|
-
</
|
|
648
|
+
</KeyboardAvoidingView>
|
|
498
649
|
);
|
|
499
650
|
}
|
|
500
651
|
|
|
501
652
|
const styles = StyleSheet.create({
|
|
502
|
-
container: { flex: 1, backgroundColor: '#
|
|
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: '
|
|
662
|
+
borderBottomColor: '#e5e5ec',
|
|
512
663
|
},
|
|
513
|
-
title: { color: '#
|
|
514
|
-
|
|
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: '
|
|
529
|
-
borderColor: '
|
|
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: '#
|
|
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: '
|
|
725
|
+
borderTopColor: '#e5e5ec',
|
|
549
726
|
padding: 10,
|
|
550
727
|
},
|
|
551
728
|
input: {
|
|
552
729
|
minHeight: 40,
|
|
553
730
|
maxHeight: 120,
|
|
554
|
-
color: '#
|
|
731
|
+
color: '#28282f',
|
|
555
732
|
fontSize: 14,
|
|
556
|
-
backgroundColor: '
|
|
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: '
|
|
752
|
+
reloadBtn: { backgroundColor: '#e9e9ef' },
|
|
574
753
|
voiceBtn: { backgroundColor: 'rgba(16,185,129,0.18)' },
|
|
575
754
|
voiceBtnActive: { backgroundColor: '#ef4444' },
|
|
576
|
-
engineToggle: { backgroundColor: '
|
|
577
|
-
engineToggleText: { color: '#
|
|
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
|
});
|