clawdex-mobile 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/npm-release.yml +18 -0
- package/AGENTS.md +3 -3
- package/README.md +104 -542
- package/apps/mobile/.env.example +1 -2
- package/apps/mobile/App.tsx +261 -68
- package/apps/mobile/app.json +31 -5
- package/apps/mobile/assets/brand/splash-icon-white.png +0 -0
- package/apps/mobile/eas.json +30 -0
- package/apps/mobile/package.json +22 -21
- package/apps/mobile/plugins/withAndroidCleartextTraffic.js +14 -0
- package/apps/mobile/src/api/__tests__/ws.test.ts +44 -6
- package/apps/mobile/src/api/chatMapping.ts +48 -8
- package/apps/mobile/src/api/client.ts +6 -0
- package/apps/mobile/src/api/types.ts +11 -0
- package/apps/mobile/src/api/ws.ts +52 -10
- package/apps/mobile/src/bridgeUrl.ts +105 -0
- package/apps/mobile/src/components/ActivityBar.tsx +32 -13
- package/apps/mobile/src/components/ChatHeader.tsx +3 -2
- package/apps/mobile/src/components/ChatInput.tsx +246 -91
- package/apps/mobile/src/components/ChatMessage.tsx +108 -4
- package/apps/mobile/src/config.ts +11 -29
- package/apps/mobile/src/hooks/useVoiceRecorder.ts +264 -0
- package/apps/mobile/src/navigation/DrawerContent.tsx +18 -8
- package/apps/mobile/src/screens/GitScreen.tsx +1 -1
- package/apps/mobile/src/screens/MainScreen.tsx +906 -268
- package/apps/mobile/src/screens/OnboardingScreen.tsx +1132 -0
- package/apps/mobile/src/screens/PrivacyScreen.tsx +1 -1
- package/apps/mobile/src/screens/SettingsScreen.tsx +65 -1
- package/apps/mobile/src/screens/TerminalScreen.tsx +1 -1
- package/apps/mobile/src/screens/TermsScreen.tsx +1 -1
- package/docs/app-review-notes.md +7 -2
- package/docs/eas-builds.md +91 -0
- package/docs/realtime-streaming-limitations.md +84 -0
- package/docs/setup-and-operations.md +239 -0
- package/docs/troubleshooting.md +121 -0
- package/docs/voice-transcription.md +87 -0
- package/package.json +8 -16
- package/scripts/setup-secure-dev.sh +122 -8
- package/scripts/setup-wizard.sh +342 -122
- package/scripts/start-bridge-secure.sh +7 -1
- package/scripts/sync-versions.js +63 -0
- package/services/rust-bridge/.env.example +1 -1
- package/services/rust-bridge/Cargo.lock +1104 -23
- package/services/rust-bridge/Cargo.toml +3 -1
- package/services/rust-bridge/package.json +1 -1
- package/services/rust-bridge/src/main.rs +587 -12
- package/apps/mobile/metro.config.js +0 -3
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ActionSheetIOS,
|
|
16
16
|
ActivityIndicator,
|
|
17
17
|
Alert,
|
|
18
|
+
FlatList,
|
|
18
19
|
Keyboard,
|
|
19
20
|
KeyboardAvoidingView,
|
|
20
21
|
Modal,
|
|
@@ -24,9 +25,13 @@ import {
|
|
|
24
25
|
StyleSheet,
|
|
25
26
|
Text,
|
|
26
27
|
TextInput,
|
|
28
|
+
type ListRenderItem,
|
|
29
|
+
type NativeScrollEvent,
|
|
30
|
+
type NativeSyntheticEvent,
|
|
27
31
|
useWindowDimensions,
|
|
28
32
|
View,
|
|
29
33
|
} from 'react-native';
|
|
34
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
30
35
|
|
|
31
36
|
import type { HostBridgeApiClient } from '../api/client';
|
|
32
37
|
import type {
|
|
@@ -57,6 +62,7 @@ import { BrandMark } from '../components/BrandMark';
|
|
|
57
62
|
import { ToolBlock } from '../components/ToolBlock';
|
|
58
63
|
import { TypingIndicator } from '../components/TypingIndicator';
|
|
59
64
|
import { env } from '../config';
|
|
65
|
+
import { useVoiceRecorder } from '../hooks/useVoiceRecorder';
|
|
60
66
|
import { colors, spacing, typography } from '../theme';
|
|
61
67
|
|
|
62
68
|
export interface MainScreenHandle {
|
|
@@ -116,6 +122,13 @@ interface ComposerAttachmentChip {
|
|
|
116
122
|
label: string;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
interface QueuedChatMessage {
|
|
126
|
+
content: string;
|
|
127
|
+
mentions: MentionInput[];
|
|
128
|
+
localImages: LocalImageInput[];
|
|
129
|
+
collaborationMode: CollaborationMode;
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
interface SlashCommandDefinition {
|
|
120
133
|
name: string;
|
|
121
134
|
summary: string;
|
|
@@ -128,25 +141,29 @@ interface SlashCommandDefinition {
|
|
|
128
141
|
const MAX_ACTIVE_COMMANDS = 16;
|
|
129
142
|
const MAX_VISIBLE_TOOL_BLOCKS = 3;
|
|
130
143
|
const RUN_WATCHDOG_MS = 60_000;
|
|
144
|
+
const CHAT_OPEN_REVEAL_DELAY_MS = 260;
|
|
145
|
+
const LARGE_CHAT_OPEN_REVEAL_DELAY_MS = 2_000;
|
|
146
|
+
const LARGE_CHAT_MESSAGE_COUNT_THRESHOLD = 120;
|
|
131
147
|
const LIKELY_RUNNING_RECENT_UPDATE_MS = 30_000;
|
|
148
|
+
const UNANSWERED_USER_RUNNING_TTL_MS = 90_000;
|
|
132
149
|
const ACTIVE_CHAT_SYNC_INTERVAL_MS = 2_000;
|
|
133
150
|
const IDLE_CHAT_SYNC_INTERVAL_MS = 2_500;
|
|
134
151
|
const CHAT_MODEL_PREFERENCES_FILE = 'chat-model-preferences.json';
|
|
135
152
|
const CHAT_MODEL_PREFERENCES_VERSION = 1;
|
|
136
153
|
const INLINE_OPTION_LINE_PATTERN =
|
|
137
154
|
/^(?:[-*+]\s*)?(?:\d{1,2}\s*[.):-]|\(\d{1,2}\)\s*[.):-]?|\[\d{1,2}\]\s*|[A-Ca-c]\s*[.):-]|\([A-Ca-c]\)\s*[.):-]?|option\s+\d{1,2}\s*[.):-]?)\s*(.+)$/i;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
const INLINE_CHOICE_CUE_PATTERNS = [
|
|
156
|
+
/\bchoose\b/i,
|
|
157
|
+
/\bselect\b/i,
|
|
158
|
+
/\bpick\b/i,
|
|
159
|
+
/\bwould you like\b/i,
|
|
160
|
+
/\bshould i\b/i,
|
|
161
|
+
/\bprefer\b/i,
|
|
162
|
+
/\bconfirm\b/i,
|
|
163
|
+
/\b(?:reply|respond)\s+with\b/i,
|
|
164
|
+
/\blet me know\b.*\b(which|what|option|one)\b/i,
|
|
165
|
+
/\bwhich\b.*\b(option|one)\b/i,
|
|
166
|
+
/\bwhat\b.*\b(option|one)\b/i,
|
|
150
167
|
];
|
|
151
168
|
const CODEX_RUN_HEARTBEAT_EVENT_TYPES = new Set([
|
|
152
169
|
'taskstarted',
|
|
@@ -173,6 +190,25 @@ const CODEX_RUN_FAILURE_EVENT_TYPES = new Set([
|
|
|
173
190
|
'taskfailed',
|
|
174
191
|
'turnfailed',
|
|
175
192
|
]);
|
|
193
|
+
const EXTERNAL_RUNNING_STATUS_HINTS = new Set([
|
|
194
|
+
'running',
|
|
195
|
+
'inprogress',
|
|
196
|
+
'active',
|
|
197
|
+
'queued',
|
|
198
|
+
'pending',
|
|
199
|
+
]);
|
|
200
|
+
const EXTERNAL_ERROR_STATUS_HINTS = new Set([
|
|
201
|
+
'failed',
|
|
202
|
+
'error',
|
|
203
|
+
'interrupted',
|
|
204
|
+
'aborted',
|
|
205
|
+
]);
|
|
206
|
+
const EXTERNAL_COMPLETE_STATUS_HINTS = new Set([
|
|
207
|
+
'complete',
|
|
208
|
+
'completed',
|
|
209
|
+
'success',
|
|
210
|
+
'succeeded',
|
|
211
|
+
]);
|
|
176
212
|
|
|
177
213
|
interface ChatModelPreference {
|
|
178
214
|
modelId: string | null;
|
|
@@ -418,15 +454,72 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
418
454
|
const [selectedEffort, setSelectedEffort] = useState<ReasoningEffort | null>(null);
|
|
419
455
|
const [selectedCollaborationMode, setSelectedCollaborationMode] =
|
|
420
456
|
useState<CollaborationMode>('default');
|
|
457
|
+
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
|
458
|
+
const [queuedMessages, setQueuedMessages] = useState<QueuedChatMessage[]>([]);
|
|
459
|
+
const [queueDispatching, setQueueDispatching] = useState(false);
|
|
460
|
+
const [queuePaused, setQueuePaused] = useState(false);
|
|
421
461
|
const [effortModalVisible, setEffortModalVisible] = useState(false);
|
|
422
462
|
const [effortPickerModelId, setEffortPickerModelId] = useState<string | null>(null);
|
|
423
463
|
const [activity, setActivity] = useState<ActivityState>({
|
|
424
464
|
tone: 'idle',
|
|
425
465
|
title: 'Ready',
|
|
426
466
|
});
|
|
427
|
-
const
|
|
467
|
+
const [composerHeight, setComposerHeight] = useState(spacing.xxl * 4);
|
|
468
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
469
|
+
const scrollRef = useRef<FlatList<ChatTranscriptMessage>>(null);
|
|
470
|
+
const scrollRetryTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
|
428
471
|
const loadChatRequestRef = useRef(0);
|
|
429
472
|
|
|
473
|
+
const voiceRecorder = useVoiceRecorder({
|
|
474
|
+
transcribe: (dataBase64, prompt, options) =>
|
|
475
|
+
api.transcribeVoice({ dataBase64, prompt, ...options }),
|
|
476
|
+
composerContext: draft,
|
|
477
|
+
onTranscript: (text) => setDraft((prev) => (prev ? `${prev} ${text}` : text)),
|
|
478
|
+
onError: (msg) => setError(msg),
|
|
479
|
+
});
|
|
480
|
+
const canUseVoiceInput = Platform.OS !== 'web';
|
|
481
|
+
|
|
482
|
+
const clearPendingScrollRetries = useCallback(() => {
|
|
483
|
+
for (const timeoutId of scrollRetryTimeoutsRef.current) {
|
|
484
|
+
clearTimeout(timeoutId);
|
|
485
|
+
}
|
|
486
|
+
scrollRetryTimeoutsRef.current = [];
|
|
487
|
+
}, []);
|
|
488
|
+
|
|
489
|
+
const scrollToBottomReliable = useCallback(
|
|
490
|
+
(animated = true) => {
|
|
491
|
+
clearPendingScrollRetries();
|
|
492
|
+
const delays = [0, 70, 180, 320];
|
|
493
|
+
scrollRetryTimeoutsRef.current = delays.map((delay, index) =>
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
requestAnimationFrame(() => {
|
|
496
|
+
scrollRef.current?.scrollToEnd({
|
|
497
|
+
animated: index === 0 ? animated : false,
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
}, delay)
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
[clearPendingScrollRetries]
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
return () => {
|
|
508
|
+
clearPendingScrollRetries();
|
|
509
|
+
};
|
|
510
|
+
}, [clearPendingScrollRetries]);
|
|
511
|
+
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
514
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
515
|
+
const showSub = Keyboard.addListener(showEvent, () => setKeyboardVisible(true));
|
|
516
|
+
const hideSub = Keyboard.addListener(hideEvent, () => setKeyboardVisible(false));
|
|
517
|
+
return () => {
|
|
518
|
+
showSub.remove();
|
|
519
|
+
hideSub.remove();
|
|
520
|
+
};
|
|
521
|
+
}, []);
|
|
522
|
+
|
|
430
523
|
// Ref so the WS handler always reads the latest chat ID without
|
|
431
524
|
// needing to re-subscribe on every change.
|
|
432
525
|
const chatIdRef = useRef<string | null>(null);
|
|
@@ -1217,6 +1310,9 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
1217
1310
|
setUploadingAttachment(false);
|
|
1218
1311
|
setActiveTurnId(null);
|
|
1219
1312
|
setStoppingTurn(false);
|
|
1313
|
+
setQueuedMessages([]);
|
|
1314
|
+
setQueueDispatching(false);
|
|
1315
|
+
setQueuePaused(false);
|
|
1220
1316
|
setActivity({
|
|
1221
1317
|
tone: 'idle',
|
|
1222
1318
|
title: 'Ready',
|
|
@@ -1846,9 +1942,9 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
1846
1942
|
],
|
|
1847
1943
|
};
|
|
1848
1944
|
});
|
|
1849
|
-
|
|
1945
|
+
scrollToBottomReliable(true);
|
|
1850
1946
|
},
|
|
1851
|
-
[selectedChatId]
|
|
1947
|
+
[scrollToBottomReliable, selectedChatId]
|
|
1852
1948
|
);
|
|
1853
1949
|
|
|
1854
1950
|
const appendLocalSystemMessage = useCallback(
|
|
@@ -1879,9 +1975,9 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
1879
1975
|
],
|
|
1880
1976
|
};
|
|
1881
1977
|
});
|
|
1882
|
-
|
|
1978
|
+
scrollToBottomReliable(true);
|
|
1883
1979
|
},
|
|
1884
|
-
[selectedChatId]
|
|
1980
|
+
[scrollToBottomReliable, selectedChatId]
|
|
1885
1981
|
);
|
|
1886
1982
|
|
|
1887
1983
|
const appendStopSystemMessageIfNeeded = useCallback(() => {
|
|
@@ -2238,7 +2334,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2238
2334
|
messages: [...prev.messages, optimisticMessage],
|
|
2239
2335
|
};
|
|
2240
2336
|
});
|
|
2241
|
-
|
|
2337
|
+
scrollToBottomReliable(true);
|
|
2242
2338
|
|
|
2243
2339
|
try {
|
|
2244
2340
|
setSending(true);
|
|
@@ -2458,6 +2554,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2458
2554
|
selectedCollaborationMode,
|
|
2459
2555
|
handleTurnFailure,
|
|
2460
2556
|
rememberChatModelPreference,
|
|
2557
|
+
scrollToBottomReliable,
|
|
2461
2558
|
startNewChat,
|
|
2462
2559
|
]
|
|
2463
2560
|
);
|
|
@@ -2466,11 +2563,15 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2466
2563
|
async (chatId: string) => {
|
|
2467
2564
|
const requestId = loadChatRequestRef.current + 1;
|
|
2468
2565
|
loadChatRequestRef.current = requestId;
|
|
2566
|
+
let loadedSuccessfully = false;
|
|
2567
|
+
let loadedMessageCount = 0;
|
|
2469
2568
|
try {
|
|
2470
2569
|
const chat = await api.getChat(chatId);
|
|
2471
2570
|
if (requestId !== loadChatRequestRef.current) {
|
|
2472
2571
|
return;
|
|
2473
2572
|
}
|
|
2573
|
+
loadedSuccessfully = true;
|
|
2574
|
+
loadedMessageCount = chat.messages.length;
|
|
2474
2575
|
setSelectedChatId(chatId);
|
|
2475
2576
|
setSelectedChat(chat);
|
|
2476
2577
|
setError(null);
|
|
@@ -2523,7 +2624,23 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2523
2624
|
detail: (err as Error).message,
|
|
2524
2625
|
});
|
|
2525
2626
|
} finally {
|
|
2526
|
-
if (requestId
|
|
2627
|
+
if (requestId !== loadChatRequestRef.current) {
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
if (loadedSuccessfully) {
|
|
2632
|
+
// Keep spinner visible until initial bottom sync settles for long threads.
|
|
2633
|
+
scrollToBottomReliable(false);
|
|
2634
|
+
const revealDelayMs =
|
|
2635
|
+
loadedMessageCount >= LARGE_CHAT_MESSAGE_COUNT_THRESHOLD
|
|
2636
|
+
? LARGE_CHAT_OPEN_REVEAL_DELAY_MS
|
|
2637
|
+
: CHAT_OPEN_REVEAL_DELAY_MS;
|
|
2638
|
+
setTimeout(() => {
|
|
2639
|
+
if (requestId === loadChatRequestRef.current) {
|
|
2640
|
+
setOpeningChatId(null);
|
|
2641
|
+
}
|
|
2642
|
+
}, revealDelayMs);
|
|
2643
|
+
} else {
|
|
2527
2644
|
setOpeningChatId(null);
|
|
2528
2645
|
}
|
|
2529
2646
|
}
|
|
@@ -2534,18 +2651,20 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2534
2651
|
bumpRunWatchdog,
|
|
2535
2652
|
clearRunWatchdog,
|
|
2536
2653
|
refreshPendingApprovalsForThread,
|
|
2654
|
+
scrollToBottomReliable,
|
|
2537
2655
|
]
|
|
2538
2656
|
);
|
|
2539
2657
|
|
|
2540
2658
|
const openChatThread = useCallback(
|
|
2541
2659
|
(id: string, optimisticChat?: Chat | null) => {
|
|
2542
|
-
const
|
|
2660
|
+
const hasSnapshot = Boolean(
|
|
2543
2661
|
optimisticChat &&
|
|
2544
2662
|
optimisticChat.id === id &&
|
|
2545
2663
|
optimisticChat.messages.length > 0
|
|
2546
2664
|
);
|
|
2547
2665
|
|
|
2548
2666
|
setSelectedChatId(id);
|
|
2667
|
+
setOpeningChatId(id);
|
|
2549
2668
|
setSending(false);
|
|
2550
2669
|
setCreating(false);
|
|
2551
2670
|
setError(null);
|
|
@@ -2560,41 +2679,19 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2560
2679
|
setActivePlan(null);
|
|
2561
2680
|
setActiveTurnId(null);
|
|
2562
2681
|
setStoppingTurn(false);
|
|
2682
|
+
setQueuedMessages([]);
|
|
2683
|
+
setQueueDispatching(false);
|
|
2684
|
+
setQueuePaused(false);
|
|
2563
2685
|
stopRequestedRef.current = false;
|
|
2564
2686
|
stopSystemMessageLoggedRef.current = false;
|
|
2565
2687
|
|
|
2566
|
-
if (
|
|
2688
|
+
if (hasSnapshot && optimisticChat) {
|
|
2567
2689
|
setSelectedChat(optimisticChat);
|
|
2568
|
-
setOpeningChatId(null);
|
|
2569
|
-
setActivity(
|
|
2570
|
-
optimisticChat.status === 'running'
|
|
2571
|
-
? {
|
|
2572
|
-
tone: 'running',
|
|
2573
|
-
title: 'Working',
|
|
2574
|
-
}
|
|
2575
|
-
: optimisticChat.status === 'complete'
|
|
2576
|
-
? {
|
|
2577
|
-
tone: 'complete',
|
|
2578
|
-
title: 'Turn completed',
|
|
2579
|
-
}
|
|
2580
|
-
: optimisticChat.status === 'error'
|
|
2581
|
-
? {
|
|
2582
|
-
tone: 'error',
|
|
2583
|
-
title: 'Turn failed',
|
|
2584
|
-
detail: optimisticChat.lastError ?? undefined,
|
|
2585
|
-
}
|
|
2586
|
-
: {
|
|
2587
|
-
tone: 'idle',
|
|
2588
|
-
title: 'Ready',
|
|
2589
|
-
}
|
|
2590
|
-
);
|
|
2591
|
-
} else {
|
|
2592
|
-
setOpeningChatId(id);
|
|
2593
|
-
setActivity({
|
|
2594
|
-
tone: 'running',
|
|
2595
|
-
title: 'Opening chat',
|
|
2596
|
-
});
|
|
2597
2690
|
}
|
|
2691
|
+
setActivity({
|
|
2692
|
+
tone: 'running',
|
|
2693
|
+
title: 'Opening chat',
|
|
2694
|
+
});
|
|
2598
2695
|
|
|
2599
2696
|
applyThreadRuntimeSnapshot(id);
|
|
2600
2697
|
void refreshPendingApprovalsForThread(id);
|
|
@@ -2687,6 +2784,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2687
2784
|
lastMessagePreview: content.slice(0, 50),
|
|
2688
2785
|
messages: [...created.messages, optimisticMessage],
|
|
2689
2786
|
});
|
|
2787
|
+
scrollToBottomReliable(true);
|
|
2690
2788
|
|
|
2691
2789
|
setActivity({
|
|
2692
2790
|
tone: 'running',
|
|
@@ -2769,6 +2867,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2769
2867
|
bumpRunWatchdog,
|
|
2770
2868
|
clearRunWatchdog,
|
|
2771
2869
|
rememberChatModelPreference,
|
|
2870
|
+
scrollToBottomReliable,
|
|
2772
2871
|
]);
|
|
2773
2872
|
|
|
2774
2873
|
const sendMessageContent = useCallback(
|
|
@@ -2777,21 +2876,29 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2777
2876
|
options?: {
|
|
2778
2877
|
allowSlashCommands?: boolean;
|
|
2779
2878
|
collaborationMode?: CollaborationMode;
|
|
2879
|
+
mentions?: MentionInput[];
|
|
2880
|
+
localImages?: LocalImageInput[];
|
|
2881
|
+
clearComposer?: boolean;
|
|
2780
2882
|
}
|
|
2781
2883
|
) => {
|
|
2782
2884
|
const content = rawContent.trim();
|
|
2783
2885
|
if (!selectedChatId || !content) {
|
|
2784
|
-
return;
|
|
2886
|
+
return false;
|
|
2785
2887
|
}
|
|
2786
2888
|
|
|
2889
|
+
const shouldClearComposer = options?.clearComposer ?? true;
|
|
2787
2890
|
if (options?.allowSlashCommands && (await handleSlashCommand(content))) {
|
|
2788
|
-
|
|
2789
|
-
|
|
2891
|
+
if (shouldClearComposer) {
|
|
2892
|
+
setDraft('');
|
|
2893
|
+
}
|
|
2894
|
+
return true;
|
|
2790
2895
|
}
|
|
2791
2896
|
const resolvedCollaborationMode =
|
|
2792
2897
|
options?.collaborationMode ?? selectedCollaborationMode;
|
|
2793
|
-
const turnMentions =
|
|
2794
|
-
|
|
2898
|
+
const turnMentions =
|
|
2899
|
+
options?.mentions ?? pendingMentionPaths.map((path) => toMentionInput(path));
|
|
2900
|
+
const turnLocalImages =
|
|
2901
|
+
options?.localImages ?? pendingLocalImagePaths.map((path) => ({ path }));
|
|
2795
2902
|
const optimisticContent = toOptimisticUserContent(content, turnMentions, turnLocalImages);
|
|
2796
2903
|
|
|
2797
2904
|
const optimisticMessage: ChatTranscriptMessage = {
|
|
@@ -2801,7 +2908,9 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2801
2908
|
createdAt: new Date().toISOString(),
|
|
2802
2909
|
};
|
|
2803
2910
|
|
|
2804
|
-
|
|
2911
|
+
if (shouldClearComposer) {
|
|
2912
|
+
setDraft('');
|
|
2913
|
+
}
|
|
2805
2914
|
setSelectedChat((prev) => {
|
|
2806
2915
|
if (!prev) return prev;
|
|
2807
2916
|
return {
|
|
@@ -2809,7 +2918,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2809
2918
|
messages: [...prev.messages, optimisticMessage],
|
|
2810
2919
|
};
|
|
2811
2920
|
});
|
|
2812
|
-
|
|
2921
|
+
scrollToBottomReliable(true);
|
|
2813
2922
|
|
|
2814
2923
|
try {
|
|
2815
2924
|
setSending(true);
|
|
@@ -2852,8 +2961,10 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2852
2961
|
selectedEffort ?? activeEffort
|
|
2853
2962
|
);
|
|
2854
2963
|
setSelectedChat(updated);
|
|
2855
|
-
|
|
2856
|
-
|
|
2964
|
+
if (shouldClearComposer) {
|
|
2965
|
+
setPendingMentionPaths([]);
|
|
2966
|
+
setPendingLocalImagePaths([]);
|
|
2967
|
+
}
|
|
2857
2968
|
setError(null);
|
|
2858
2969
|
if (updated.status === 'complete') {
|
|
2859
2970
|
setActivity({
|
|
@@ -2882,9 +2993,12 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2882
2993
|
}
|
|
2883
2994
|
} catch (err) {
|
|
2884
2995
|
handleTurnFailure(err);
|
|
2996
|
+
return false;
|
|
2885
2997
|
} finally {
|
|
2886
2998
|
setSending(false);
|
|
2887
2999
|
}
|
|
3000
|
+
|
|
3001
|
+
return true;
|
|
2888
3002
|
},
|
|
2889
3003
|
[
|
|
2890
3004
|
activeEffort,
|
|
@@ -2902,12 +3016,124 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2902
3016
|
bumpRunWatchdog,
|
|
2903
3017
|
clearRunWatchdog,
|
|
2904
3018
|
rememberChatModelPreference,
|
|
3019
|
+
scrollToBottomReliable,
|
|
2905
3020
|
]
|
|
2906
3021
|
);
|
|
2907
3022
|
|
|
2908
3023
|
const sendMessage = useCallback(async () => {
|
|
2909
|
-
|
|
2910
|
-
|
|
3024
|
+
const content = draft.trim();
|
|
3025
|
+
if (!content) {
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
setQueuePaused(false);
|
|
3030
|
+
|
|
3031
|
+
if (uploadingAttachment) {
|
|
3032
|
+
setError('Please wait for attachments to finish uploading.');
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
if (await handleSlashCommand(content)) {
|
|
3037
|
+
setDraft('');
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
const isTurnBlocked =
|
|
3042
|
+
sending ||
|
|
3043
|
+
creating ||
|
|
3044
|
+
stoppingTurn ||
|
|
3045
|
+
Boolean(activeTurnIdRef.current) ||
|
|
3046
|
+
Boolean(pendingApproval?.id) ||
|
|
3047
|
+
Boolean(pendingUserInputRequest?.id) ||
|
|
3048
|
+
(selectedChat ? isChatLikelyRunning(selectedChat) : false);
|
|
3049
|
+
|
|
3050
|
+
if (isTurnBlocked) {
|
|
3051
|
+
const queuedMentions = pendingMentionPaths.map((path) => toMentionInput(path));
|
|
3052
|
+
const queuedLocalImages = pendingLocalImagePaths.map((path) => ({ path }));
|
|
3053
|
+
setQueuedMessages((prev) => [
|
|
3054
|
+
...prev,
|
|
3055
|
+
{
|
|
3056
|
+
content,
|
|
3057
|
+
mentions: queuedMentions,
|
|
3058
|
+
localImages: queuedLocalImages,
|
|
3059
|
+
collaborationMode: selectedCollaborationMode,
|
|
3060
|
+
},
|
|
3061
|
+
]);
|
|
3062
|
+
setDraft('');
|
|
3063
|
+
setPendingMentionPaths([]);
|
|
3064
|
+
setPendingLocalImagePaths([]);
|
|
3065
|
+
setError(null);
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
await sendMessageContent(content, { allowSlashCommands: false });
|
|
3070
|
+
}, [
|
|
3071
|
+
creating,
|
|
3072
|
+
draft,
|
|
3073
|
+
handleSlashCommand,
|
|
3074
|
+
pendingApproval?.id,
|
|
3075
|
+
pendingLocalImagePaths,
|
|
3076
|
+
pendingMentionPaths,
|
|
3077
|
+
pendingUserInputRequest?.id,
|
|
3078
|
+
selectedChat,
|
|
3079
|
+
selectedCollaborationMode,
|
|
3080
|
+
sendMessageContent,
|
|
3081
|
+
sending,
|
|
3082
|
+
stoppingTurn,
|
|
3083
|
+
setQueuePaused,
|
|
3084
|
+
uploadingAttachment,
|
|
3085
|
+
]);
|
|
3086
|
+
|
|
3087
|
+
useEffect(() => {
|
|
3088
|
+
if (!selectedChatId || queuedMessages.length === 0 || queueDispatching || queuePaused) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
const isTurnBlocked =
|
|
3093
|
+
sending ||
|
|
3094
|
+
creating ||
|
|
3095
|
+
stoppingTurn ||
|
|
3096
|
+
uploadingAttachment ||
|
|
3097
|
+
Boolean(activeTurnId) ||
|
|
3098
|
+
Boolean(pendingApproval?.id) ||
|
|
3099
|
+
Boolean(pendingUserInputRequest?.id) ||
|
|
3100
|
+
(selectedChat ? isChatLikelyRunning(selectedChat) : false);
|
|
3101
|
+
if (isTurnBlocked) {
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const nextMessage = queuedMessages[0];
|
|
3106
|
+
setQueueDispatching(true);
|
|
3107
|
+
void (async () => {
|
|
3108
|
+
const sent = await sendMessageContent(nextMessage.content, {
|
|
3109
|
+
allowSlashCommands: false,
|
|
3110
|
+
collaborationMode: nextMessage.collaborationMode,
|
|
3111
|
+
mentions: nextMessage.mentions,
|
|
3112
|
+
localImages: nextMessage.localImages,
|
|
3113
|
+
clearComposer: false,
|
|
3114
|
+
});
|
|
3115
|
+
if (sent) {
|
|
3116
|
+
setQueuedMessages((prev) => prev.slice(1));
|
|
3117
|
+
} else {
|
|
3118
|
+
setQueuePaused(true);
|
|
3119
|
+
}
|
|
3120
|
+
setQueueDispatching(false);
|
|
3121
|
+
})();
|
|
3122
|
+
}, [
|
|
3123
|
+
activeTurnId,
|
|
3124
|
+
creating,
|
|
3125
|
+
pendingApproval?.id,
|
|
3126
|
+
pendingUserInputRequest?.id,
|
|
3127
|
+
queueDispatching,
|
|
3128
|
+
queuePaused,
|
|
3129
|
+
queuedMessages,
|
|
3130
|
+
selectedChat,
|
|
3131
|
+
selectedChatId,
|
|
3132
|
+
sendMessageContent,
|
|
3133
|
+
sending,
|
|
3134
|
+
stoppingTurn,
|
|
3135
|
+
uploadingAttachment,
|
|
3136
|
+
]);
|
|
2911
3137
|
|
|
2912
3138
|
const handleInlineOptionSelect = useCallback(
|
|
2913
3139
|
(value: string) => {
|
|
@@ -2920,8 +3146,11 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2920
3146
|
!selectedChatId ||
|
|
2921
3147
|
sending ||
|
|
2922
3148
|
creating ||
|
|
3149
|
+
stoppingTurn ||
|
|
3150
|
+
Boolean(activeTurnId) ||
|
|
2923
3151
|
Boolean(pendingApproval?.id) ||
|
|
2924
|
-
Boolean(pendingUserInputRequest?.id)
|
|
3152
|
+
Boolean(pendingUserInputRequest?.id) ||
|
|
3153
|
+
(selectedChat ? isChatLikelyRunning(selectedChat) : false);
|
|
2925
3154
|
if (cannotAutoSend) {
|
|
2926
3155
|
setDraft(option);
|
|
2927
3156
|
return;
|
|
@@ -2931,11 +3160,14 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2931
3160
|
},
|
|
2932
3161
|
[
|
|
2933
3162
|
creating,
|
|
3163
|
+
activeTurnId,
|
|
2934
3164
|
pendingApproval?.id,
|
|
2935
3165
|
pendingUserInputRequest?.id,
|
|
3166
|
+
selectedChat,
|
|
2936
3167
|
selectedChatId,
|
|
2937
3168
|
sendMessageContent,
|
|
2938
3169
|
sending,
|
|
3170
|
+
stoppingTurn,
|
|
2939
3171
|
]
|
|
2940
3172
|
);
|
|
2941
3173
|
|
|
@@ -2948,7 +3180,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2948
3180
|
|
|
2949
3181
|
if (event.method === 'thread/name/updated') {
|
|
2950
3182
|
const params = toRecord(event.params);
|
|
2951
|
-
const threadId =
|
|
3183
|
+
const threadId = extractNotificationThreadId(params);
|
|
2952
3184
|
if (!threadId || threadId !== currentId) {
|
|
2953
3185
|
return;
|
|
2954
3186
|
}
|
|
@@ -2979,15 +3211,12 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
2979
3211
|
if (!codexEventType) {
|
|
2980
3212
|
return;
|
|
2981
3213
|
}
|
|
2982
|
-
const threadId =
|
|
2983
|
-
readString(msg?.thread_id) ??
|
|
2984
|
-
readString(msg?.threadId) ??
|
|
2985
|
-
readString(params?.thread_id) ??
|
|
2986
|
-
readString(params?.threadId) ??
|
|
2987
|
-
readString(params?.conversationId) ??
|
|
2988
|
-
readString(msg?.conversation_id);
|
|
3214
|
+
const threadId = extractNotificationThreadId(params, msg);
|
|
2989
3215
|
|
|
2990
3216
|
if (!currentId) {
|
|
3217
|
+
if (threadId) {
|
|
3218
|
+
cacheCodexRuntimeForThread(threadId, codexEventType, msg);
|
|
3219
|
+
}
|
|
2991
3220
|
return;
|
|
2992
3221
|
}
|
|
2993
3222
|
|
|
@@ -3077,7 +3306,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
3077
3306
|
title: 'Thinking',
|
|
3078
3307
|
}
|
|
3079
3308
|
);
|
|
3080
|
-
|
|
3309
|
+
scrollToBottomReliable(true);
|
|
3081
3310
|
return;
|
|
3082
3311
|
}
|
|
3083
3312
|
|
|
@@ -3271,7 +3500,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
3271
3500
|
title: 'Thinking',
|
|
3272
3501
|
}
|
|
3273
3502
|
);
|
|
3274
|
-
|
|
3503
|
+
scrollToBottomReliable(true);
|
|
3275
3504
|
return;
|
|
3276
3505
|
}
|
|
3277
3506
|
|
|
@@ -4056,9 +4285,26 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4056
4285
|
// wipe streaming text, active commands, and the watchdog.
|
|
4057
4286
|
if (event.method === 'thread/status/changed') {
|
|
4058
4287
|
const params = toRecord(event.params);
|
|
4059
|
-
const threadId =
|
|
4060
|
-
|
|
4288
|
+
const threadId = extractNotificationThreadId(params);
|
|
4289
|
+
const statusHint = extractExternalStatusHint(params);
|
|
4290
|
+
const hasExplicitRunningStatus = Boolean(
|
|
4291
|
+
statusHint && EXTERNAL_RUNNING_STATUS_HINTS.has(statusHint)
|
|
4292
|
+
);
|
|
4293
|
+
const hasExplicitTerminalStatus = Boolean(
|
|
4294
|
+
statusHint &&
|
|
4295
|
+
(EXTERNAL_ERROR_STATUS_HINTS.has(statusHint) ||
|
|
4296
|
+
EXTERNAL_COMPLETE_STATUS_HINTS.has(statusHint))
|
|
4297
|
+
);
|
|
4061
4298
|
if (threadId && threadId === currentId) {
|
|
4299
|
+
if (!hasExplicitTerminalStatus) {
|
|
4300
|
+
bumpRunWatchdog();
|
|
4301
|
+
setActivity((prev) =>
|
|
4302
|
+
prev.tone === 'running'
|
|
4303
|
+
? prev
|
|
4304
|
+
: { tone: 'running', title: 'Working' }
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4062
4308
|
api
|
|
4063
4309
|
.getChatSummary(threadId)
|
|
4064
4310
|
.then((summary) => {
|
|
@@ -4077,26 +4323,79 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4077
4323
|
};
|
|
4078
4324
|
});
|
|
4079
4325
|
|
|
4080
|
-
|
|
4326
|
+
const shouldPreserveRunning =
|
|
4327
|
+
!hasExplicitTerminalStatus &&
|
|
4328
|
+
runWatchdogUntilRef.current > Date.now();
|
|
4329
|
+
const shouldShowRunning =
|
|
4330
|
+
hasExplicitRunningStatus ||
|
|
4331
|
+
isChatSummaryLikelyRunning(summary) ||
|
|
4332
|
+
shouldPreserveRunning;
|
|
4333
|
+
|
|
4334
|
+
if (shouldShowRunning) {
|
|
4081
4335
|
bumpRunWatchdog();
|
|
4082
4336
|
setActivity((prev) =>
|
|
4083
4337
|
prev.tone === 'running'
|
|
4084
4338
|
? prev
|
|
4085
4339
|
: { tone: 'running', title: 'Working' }
|
|
4086
4340
|
);
|
|
4341
|
+
} else {
|
|
4342
|
+
clearRunWatchdog();
|
|
4343
|
+
setActiveTurnId(null);
|
|
4344
|
+
setStoppingTurn(false);
|
|
4345
|
+
if (!pendingApprovalId && !pendingUserInputRequestId) {
|
|
4346
|
+
setActiveCommands([]);
|
|
4347
|
+
setStreamingText(null);
|
|
4348
|
+
reasoningSummaryRef.current = {};
|
|
4349
|
+
codexReasoningBufferRef.current = '';
|
|
4350
|
+
hadCommandRef.current = false;
|
|
4351
|
+
setActivity(() => {
|
|
4352
|
+
if (statusHint && EXTERNAL_ERROR_STATUS_HINTS.has(statusHint)) {
|
|
4353
|
+
return {
|
|
4354
|
+
tone: 'error',
|
|
4355
|
+
title: 'Turn failed',
|
|
4356
|
+
detail: summary.lastError ?? undefined,
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
if (statusHint && EXTERNAL_COMPLETE_STATUS_HINTS.has(statusHint)) {
|
|
4361
|
+
return {
|
|
4362
|
+
tone: 'complete',
|
|
4363
|
+
title: 'Turn completed',
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
return summary.status === 'error'
|
|
4368
|
+
? {
|
|
4369
|
+
tone: 'error',
|
|
4370
|
+
title: 'Turn failed',
|
|
4371
|
+
detail: summary.lastError ?? undefined,
|
|
4372
|
+
}
|
|
4373
|
+
: summary.status === 'complete'
|
|
4374
|
+
? {
|
|
4375
|
+
tone: 'complete',
|
|
4376
|
+
title: 'Turn completed',
|
|
4377
|
+
}
|
|
4378
|
+
: {
|
|
4379
|
+
tone: 'idle',
|
|
4380
|
+
title: 'Ready',
|
|
4381
|
+
};
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4087
4384
|
}
|
|
4088
4385
|
})
|
|
4089
4386
|
.catch(() => {});
|
|
4090
4387
|
|
|
4091
4388
|
scheduleExternalStatusFullSync(threadId);
|
|
4092
4389
|
} else if (threadId) {
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4390
|
+
if (!hasExplicitTerminalStatus) {
|
|
4391
|
+
cacheThreadTurnState(threadId, {
|
|
4392
|
+
runWatchdogUntil: Date.now() + RUN_WATCHDOG_MS,
|
|
4393
|
+
});
|
|
4394
|
+
cacheThreadActivity(threadId, {
|
|
4395
|
+
tone: 'running',
|
|
4396
|
+
title: 'Working',
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4100
4399
|
void refreshPendingApprovalsForThread(threadId);
|
|
4101
4400
|
}
|
|
4102
4401
|
return;
|
|
@@ -4148,6 +4447,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4148
4447
|
scheduleExternalStatusFullSync,
|
|
4149
4448
|
registerTurnStarted,
|
|
4150
4449
|
pushActiveCommand,
|
|
4450
|
+
scrollToBottomReliable,
|
|
4151
4451
|
upsertThreadRuntimeSnapshot,
|
|
4152
4452
|
]);
|
|
4153
4453
|
|
|
@@ -4179,9 +4479,19 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4179
4479
|
return isUnchanged ? prev : latest;
|
|
4180
4480
|
});
|
|
4181
4481
|
|
|
4182
|
-
const
|
|
4482
|
+
const hasAssistantProgress = didAssistantMessageProgress(selectedChat, latest);
|
|
4483
|
+
const hasPendingUserMessage = hasRecentUnansweredUserTurn(latest);
|
|
4484
|
+
const shouldRunFromChat =
|
|
4485
|
+
isChatLikelyRunning(latest) ||
|
|
4486
|
+
hasAssistantProgress ||
|
|
4487
|
+
hasPendingUserMessage;
|
|
4183
4488
|
const shouldRunFromWatchdog = runWatchdogUntilRef.current > Date.now();
|
|
4184
4489
|
const shouldShowRunning = shouldRunFromChat || shouldRunFromWatchdog;
|
|
4490
|
+
const shouldRefreshWatchdog = shouldRunFromChat;
|
|
4491
|
+
const watchdogDurationMs =
|
|
4492
|
+
hasAssistantProgress && !isChatLikelyRunning(latest)
|
|
4493
|
+
? Math.floor(RUN_WATCHDOG_MS / 4)
|
|
4494
|
+
: RUN_WATCHDOG_MS;
|
|
4185
4495
|
|
|
4186
4496
|
if (shouldShowRunning && !hasPendingApproval && !hasPendingUserInput) {
|
|
4187
4497
|
setActivity((prev) => {
|
|
@@ -4194,13 +4504,22 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4194
4504
|
) {
|
|
4195
4505
|
return prev;
|
|
4196
4506
|
}
|
|
4197
|
-
|
|
4507
|
+
if (shouldRefreshWatchdog) {
|
|
4508
|
+
bumpRunWatchdog(watchdogDurationMs);
|
|
4509
|
+
}
|
|
4198
4510
|
return prev.tone === 'running'
|
|
4199
4511
|
? prev
|
|
4200
|
-
: { tone: 'running', title: 'Working' };
|
|
4512
|
+
: { tone: 'running', title: hasAssistantProgress ? 'Thinking' : 'Working' };
|
|
4201
4513
|
});
|
|
4202
4514
|
} else if (!hasPendingApproval && !hasPendingUserInput) {
|
|
4203
4515
|
clearRunWatchdog();
|
|
4516
|
+
setActiveCommands([]);
|
|
4517
|
+
setStreamingText(null);
|
|
4518
|
+
setActiveTurnId(null);
|
|
4519
|
+
setStoppingTurn(false);
|
|
4520
|
+
reasoningSummaryRef.current = {};
|
|
4521
|
+
codexReasoningBufferRef.current = '';
|
|
4522
|
+
hadCommandRef.current = false;
|
|
4204
4523
|
setActivity((prev) => {
|
|
4205
4524
|
if (latest.status === 'error') {
|
|
4206
4525
|
return {
|
|
@@ -4344,32 +4663,52 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4344
4663
|
|
|
4345
4664
|
const handleComposerFocus = useCallback(() => {
|
|
4346
4665
|
requestAnimationFrame(() => {
|
|
4347
|
-
|
|
4666
|
+
scrollToBottomReliable(true);
|
|
4348
4667
|
});
|
|
4349
|
-
}, []);
|
|
4668
|
+
}, [scrollToBottomReliable]);
|
|
4350
4669
|
|
|
4351
4670
|
const handleSubmit = selectedChat ? sendMessage : createChat;
|
|
4352
4671
|
const isTurnLoading = sending || creating;
|
|
4353
4672
|
const isLoading = isTurnLoading || uploadingAttachment;
|
|
4354
4673
|
const isStreaming = sending || creating || Boolean(streamingText);
|
|
4355
4674
|
const isOpeningChat = Boolean(openingChatId);
|
|
4356
|
-
const
|
|
4357
|
-
Boolean(openingChatId) && selectedChat?.id !== openingChatId;
|
|
4675
|
+
const shouldShowComposer = !isOpeningChat;
|
|
4358
4676
|
const isTurnLikelyRunning =
|
|
4359
4677
|
Boolean(activeTurnId) || (selectedChat ? isChatLikelyRunning(selectedChat) : false);
|
|
4678
|
+
const queuedMessagesDetail =
|
|
4679
|
+
queuedMessages.length > 0
|
|
4680
|
+
? queuePaused
|
|
4681
|
+
? `${String(queuedMessages.length)} queued (paused)`
|
|
4682
|
+
: `${String(queuedMessages.length)} queued`
|
|
4683
|
+
: undefined;
|
|
4684
|
+
const activityDetail = queuedMessagesDetail
|
|
4685
|
+
? activity.detail
|
|
4686
|
+
? `${activity.detail} · ${queuedMessagesDetail}`
|
|
4687
|
+
: queuedMessagesDetail
|
|
4688
|
+
: activity.detail;
|
|
4360
4689
|
const showActivity =
|
|
4361
4690
|
isLoading ||
|
|
4362
4691
|
isOpeningChat ||
|
|
4692
|
+
Boolean(queuedMessagesDetail) ||
|
|
4363
4693
|
activity.tone !== 'idle' ||
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
const headerTitle = isOpeningDifferentChat
|
|
4367
|
-
? 'Opening chat'
|
|
4368
|
-
: selectedChat?.title?.trim() || 'New chat';
|
|
4694
|
+
Boolean(activityDetail);
|
|
4695
|
+
const headerTitle = isOpeningChat ? 'Opening chat' : selectedChat?.title?.trim() || 'New chat';
|
|
4369
4696
|
const workspaceLabel = selectedChat?.cwd?.trim() || 'Workspace not set';
|
|
4370
4697
|
const defaultStartWorkspaceLabel =
|
|
4371
4698
|
preferredStartCwd ?? 'Bridge default workspace';
|
|
4372
4699
|
const showSlashSuggestions = slashSuggestions.length > 0 && draft.trimStart().startsWith('/');
|
|
4700
|
+
const showFloatingActivity =
|
|
4701
|
+
showActivity && shouldShowComposer && Boolean(selectedChat) && !isOpeningChat;
|
|
4702
|
+
const chatBottomInset = shouldShowComposer
|
|
4703
|
+
? spacing.lg + (showFloatingActivity ? spacing.xxl + spacing.sm : 0)
|
|
4704
|
+
: Math.max(spacing.xxl, safeAreaInsets.bottom + spacing.lg);
|
|
4705
|
+
|
|
4706
|
+
useEffect(() => {
|
|
4707
|
+
if (!selectedChat || isOpeningChat || !showActivity) {
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
scrollToBottomReliable(false);
|
|
4711
|
+
}, [isOpeningChat, scrollToBottomReliable, selectedChat, showActivity]);
|
|
4373
4712
|
|
|
4374
4713
|
return (
|
|
4375
4714
|
<View style={styles.container}>
|
|
@@ -4381,7 +4720,7 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4381
4720
|
onRightActionPress={selectedChat ? handleOpenGit : undefined}
|
|
4382
4721
|
/>
|
|
4383
4722
|
|
|
4384
|
-
{selectedChat ? (
|
|
4723
|
+
{selectedChat && !isOpeningChat ? (
|
|
4385
4724
|
<View style={styles.sessionMetaRow}>
|
|
4386
4725
|
<Pressable style={styles.workspaceBar} onPress={handleOpenGit}>
|
|
4387
4726
|
<Ionicons name="folder-open-outline" size={14} color={colors.textMuted} />
|
|
@@ -4418,9 +4757,10 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4418
4757
|
|
|
4419
4758
|
<KeyboardAvoidingView
|
|
4420
4759
|
style={styles.keyboardAvoiding}
|
|
4421
|
-
behavior={Platform.OS === 'ios' ? 'padding' :
|
|
4760
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
4761
|
+
enabled={Platform.OS === 'ios'}
|
|
4422
4762
|
>
|
|
4423
|
-
{selectedChat && !
|
|
4763
|
+
{selectedChat && !isOpeningChat ? (
|
|
4424
4764
|
<ChatView
|
|
4425
4765
|
chat={selectedChat}
|
|
4426
4766
|
activePlan={activePlan?.threadId === selectedChat.id ? activePlan : null}
|
|
@@ -4430,6 +4770,8 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4430
4770
|
isStreaming={isStreaming}
|
|
4431
4771
|
inlineChoicesEnabled={!pendingUserInputRequest && !pendingApproval && !isLoading}
|
|
4432
4772
|
onInlineOptionSelect={handleInlineOptionSelect}
|
|
4773
|
+
onAutoScroll={scrollToBottomReliable}
|
|
4774
|
+
bottomInset={chatBottomInset}
|
|
4433
4775
|
/>
|
|
4434
4776
|
) : isOpeningChat ? (
|
|
4435
4777
|
<View style={styles.chatLoadingContainer}>
|
|
@@ -4448,70 +4790,96 @@ export const MainScreen = forwardRef<MainScreenHandle, MainScreenProps>(
|
|
|
4448
4790
|
/>
|
|
4449
4791
|
)}
|
|
4450
4792
|
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
{showActivity ? (
|
|
4793
|
+
{showFloatingActivity ? (
|
|
4794
|
+
<View
|
|
4795
|
+
pointerEvents="none"
|
|
4796
|
+
style={[
|
|
4797
|
+
styles.activityOverlay,
|
|
4798
|
+
{ bottom: composerHeight + spacing.sm },
|
|
4799
|
+
]}
|
|
4800
|
+
>
|
|
4460
4801
|
<ActivityBar
|
|
4461
4802
|
title={activity.title}
|
|
4462
|
-
detail={
|
|
4803
|
+
detail={activityDetail}
|
|
4463
4804
|
tone={activity.tone}
|
|
4464
4805
|
/>
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4806
|
+
</View>
|
|
4807
|
+
) : null}
|
|
4808
|
+
|
|
4809
|
+
{shouldShowComposer ? (
|
|
4810
|
+
<View
|
|
4811
|
+
style={[
|
|
4812
|
+
styles.composerContainer,
|
|
4813
|
+
!keyboardVisible ? styles.composerContainerResting : null,
|
|
4814
|
+
]}
|
|
4815
|
+
onLayout={(event) => {
|
|
4816
|
+
const nextHeight = Math.ceil(event.nativeEvent.layout.height);
|
|
4817
|
+
setComposerHeight((previousHeight) =>
|
|
4818
|
+
previousHeight === nextHeight ? previousHeight : nextHeight
|
|
4819
|
+
);
|
|
4820
|
+
}}
|
|
4821
|
+
>
|
|
4822
|
+
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
|
4823
|
+
{pendingApproval ? (
|
|
4824
|
+
<ApprovalBanner
|
|
4825
|
+
approval={pendingApproval}
|
|
4826
|
+
onResolve={handleResolveApproval}
|
|
4827
|
+
/>
|
|
4828
|
+
) : null}
|
|
4829
|
+
{showSlashSuggestions ? (
|
|
4830
|
+
<ScrollView
|
|
4831
|
+
style={[
|
|
4832
|
+
styles.slashSuggestions,
|
|
4833
|
+
{ maxHeight: slashSuggestionsMaxHeight },
|
|
4834
|
+
]}
|
|
4835
|
+
contentContainerStyle={styles.slashSuggestionsContent}
|
|
4836
|
+
keyboardShouldPersistTaps="handled"
|
|
4837
|
+
nestedScrollEnabled
|
|
4838
|
+
>
|
|
4839
|
+
{slashSuggestions.map((command, index) => {
|
|
4840
|
+
const suffix = command.argsHint ? ` ${command.argsHint}` : '';
|
|
4841
|
+
return (
|
|
4842
|
+
<Pressable
|
|
4843
|
+
key={`${command.name}-${String(index)}`}
|
|
4844
|
+
onPress={() => setDraft(`/${command.name}${command.argsHint ? ' ' : ''}`)}
|
|
4845
|
+
style={({ pressed }) => [
|
|
4846
|
+
styles.slashSuggestionItem,
|
|
4847
|
+
index === slashSuggestions.length - 1 &&
|
|
4848
|
+
styles.slashSuggestionItemLast,
|
|
4849
|
+
pressed && styles.slashSuggestionItemPressed,
|
|
4850
|
+
]}
|
|
4851
|
+
>
|
|
4852
|
+
<Text style={styles.slashSuggestionTitle}>{`/${command.name}${suffix}`}</Text>
|
|
4853
|
+
<Text style={styles.slashSuggestionSummary} numberOfLines={1}>
|
|
4854
|
+
{command.mobileSupported
|
|
4855
|
+
? command.summary
|
|
4856
|
+
: `${command.summary} · CLI only`}
|
|
4857
|
+
</Text>
|
|
4858
|
+
</Pressable>
|
|
4859
|
+
);
|
|
4860
|
+
})}
|
|
4861
|
+
</ScrollView>
|
|
4862
|
+
) : null}
|
|
4863
|
+
<ChatInput
|
|
4864
|
+
value={draft}
|
|
4865
|
+
onChangeText={setDraft}
|
|
4866
|
+
onFocus={handleComposerFocus}
|
|
4867
|
+
onSubmit={() => void handleSubmit()}
|
|
4868
|
+
onStop={() => handleStopTurn()}
|
|
4869
|
+
showStopButton={isTurnLoading || isTurnLikelyRunning || stoppingTurn}
|
|
4870
|
+
isStopping={stoppingTurn}
|
|
4871
|
+
onAttachPress={openAttachmentMenu}
|
|
4872
|
+
attachments={composerAttachments}
|
|
4873
|
+
onRemoveAttachment={removeComposerAttachment}
|
|
4874
|
+
isLoading={isLoading}
|
|
4875
|
+
placeholder={selectedChat ? 'Reply...' : 'Message Codex...'}
|
|
4876
|
+
voiceState={canUseVoiceInput ? voiceRecorder.voiceState : 'idle'}
|
|
4877
|
+
onVoiceToggle={canUseVoiceInput ? voiceRecorder.toggleRecording : undefined}
|
|
4878
|
+
safeAreaBottomInset={safeAreaInsets.bottom}
|
|
4879
|
+
keyboardVisible={keyboardVisible}
|
|
4880
|
+
/>
|
|
4881
|
+
</View>
|
|
4882
|
+
) : null}
|
|
4515
4883
|
</KeyboardAvoidingView>
|
|
4516
4884
|
|
|
4517
4885
|
<Modal
|
|
@@ -5055,133 +5423,221 @@ function ChatView({
|
|
|
5055
5423
|
isStreaming,
|
|
5056
5424
|
inlineChoicesEnabled,
|
|
5057
5425
|
onInlineOptionSelect,
|
|
5426
|
+
onAutoScroll,
|
|
5427
|
+
bottomInset,
|
|
5058
5428
|
}: {
|
|
5059
5429
|
chat: Chat;
|
|
5060
5430
|
activePlan: ActivePlanState | null;
|
|
5061
5431
|
activeCommands: RunEvent[];
|
|
5062
5432
|
streamingText: string | null;
|
|
5063
|
-
scrollRef: React.RefObject<
|
|
5433
|
+
scrollRef: React.RefObject<FlatList<ChatTranscriptMessage> | null>;
|
|
5064
5434
|
isStreaming: boolean;
|
|
5065
5435
|
inlineChoicesEnabled: boolean;
|
|
5066
5436
|
onInlineOptionSelect: (value: string) => void;
|
|
5437
|
+
onAutoScroll: (animated?: boolean) => void;
|
|
5438
|
+
bottomInset: number;
|
|
5067
5439
|
}) {
|
|
5068
5440
|
const { height: windowHeight } = useWindowDimensions();
|
|
5069
|
-
const
|
|
5441
|
+
const shouldStickToBottomRef = useRef(true);
|
|
5442
|
+
const visibleToolBlocks = useMemo(
|
|
5443
|
+
() => activeCommands.slice(-MAX_VISIBLE_TOOL_BLOCKS),
|
|
5444
|
+
[activeCommands]
|
|
5445
|
+
);
|
|
5070
5446
|
const toolPanelMaxHeight = Math.floor(windowHeight * 0.5);
|
|
5071
|
-
const liveTimelineText = toLiveTimelineText(activeCommands);
|
|
5447
|
+
const liveTimelineText = useMemo(() => toLiveTimelineText(activeCommands), [activeCommands]);
|
|
5072
5448
|
const shouldShowToolPanel = visibleToolBlocks.length > 0 && !liveTimelineText;
|
|
5073
5449
|
|
|
5074
|
-
const
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5450
|
+
const visibleMessages = useMemo(() => {
|
|
5451
|
+
const filtered = chat.messages.filter((msg) => {
|
|
5452
|
+
const text = msg.content || '';
|
|
5453
|
+
if (msg.role === 'system') return false;
|
|
5454
|
+
if (text.includes('FINAL_TASK_RESULT_JSON')) return false;
|
|
5455
|
+
if (text.includes('Current working directory is:')) return false;
|
|
5456
|
+
if (text.includes('You are operating in task worktree')) return false;
|
|
5457
|
+
if (msg.role === 'assistant' && !text.trim()) return false;
|
|
5458
|
+
return true;
|
|
5459
|
+
});
|
|
5083
5460
|
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
: null
|
|
5094
|
-
|
|
5461
|
+
// For each consecutive run of assistant messages, only keep the last
|
|
5462
|
+
// one (the final answer). Earlier ones are intermediate thinking.
|
|
5463
|
+
return filtered.filter((msg, i) => {
|
|
5464
|
+
if (msg.role !== 'assistant') return true;
|
|
5465
|
+
const next = filtered[i + 1];
|
|
5466
|
+
return !next || next.role !== 'assistant';
|
|
5467
|
+
});
|
|
5468
|
+
}, [chat.messages]);
|
|
5469
|
+
const inlineChoiceSet = useMemo(
|
|
5470
|
+
() => (inlineChoicesEnabled ? findInlineChoiceSet(visibleMessages) : null),
|
|
5471
|
+
[inlineChoicesEnabled, visibleMessages]
|
|
5472
|
+
);
|
|
5473
|
+
const streamingPreviewText = useMemo(
|
|
5474
|
+
() => toStreamingPreviewText(streamingText, visibleMessages),
|
|
5475
|
+
[streamingText, visibleMessages]
|
|
5476
|
+
);
|
|
5477
|
+
const initialBottomSyncChatIdRef = useRef<string | null>(null);
|
|
5478
|
+
const handleScroll = useCallback(
|
|
5479
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
5480
|
+
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
5481
|
+
const distanceFromBottom =
|
|
5482
|
+
contentSize.height - (contentOffset.y + layoutMeasurement.height);
|
|
5483
|
+
shouldStickToBottomRef.current = distanceFromBottom <= spacing.xl * 2;
|
|
5484
|
+
},
|
|
5485
|
+
[]
|
|
5486
|
+
);
|
|
5095
5487
|
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
</View>
|
|
5139
|
-
) : null}
|
|
5140
|
-
</View>
|
|
5141
|
-
);
|
|
5142
|
-
})}
|
|
5143
|
-
{liveTimelineText ? (
|
|
5488
|
+
useEffect(() => {
|
|
5489
|
+
if (initialBottomSyncChatIdRef.current === chat.id) {
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
if (!activePlan && visibleMessages.length === 0 && !liveTimelineText && !streamingPreviewText) {
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
initialBottomSyncChatIdRef.current = chat.id;
|
|
5497
|
+
const scrollToBottom = () => onAutoScroll(false);
|
|
5498
|
+
const frame = requestAnimationFrame(scrollToBottom);
|
|
5499
|
+
const timeout = setTimeout(scrollToBottom, 120);
|
|
5500
|
+
|
|
5501
|
+
return () => {
|
|
5502
|
+
cancelAnimationFrame(frame);
|
|
5503
|
+
clearTimeout(timeout);
|
|
5504
|
+
};
|
|
5505
|
+
}, [
|
|
5506
|
+
activePlan,
|
|
5507
|
+
chat.id,
|
|
5508
|
+
liveTimelineText,
|
|
5509
|
+
onAutoScroll,
|
|
5510
|
+
scrollRef,
|
|
5511
|
+
streamingPreviewText,
|
|
5512
|
+
visibleMessages.length,
|
|
5513
|
+
]);
|
|
5514
|
+
|
|
5515
|
+
useEffect(() => {
|
|
5516
|
+
shouldStickToBottomRef.current = true;
|
|
5517
|
+
}, [chat.id]);
|
|
5518
|
+
|
|
5519
|
+
const messageListContentStyle = useMemo(
|
|
5520
|
+
() => [styles.messageListContent, { paddingBottom: bottomInset }],
|
|
5521
|
+
[bottomInset]
|
|
5522
|
+
);
|
|
5523
|
+
const isLargeChat = visibleMessages.length >= LARGE_CHAT_MESSAGE_COUNT_THRESHOLD;
|
|
5524
|
+
const aggressiveRenderBatchSize = Math.max(visibleMessages.length, 1);
|
|
5525
|
+
const keyExtractor = useCallback((msg: ChatTranscriptMessage) => msg.id, []);
|
|
5526
|
+
const renderMessageItem = useCallback<ListRenderItem<ChatTranscriptMessage>>(
|
|
5527
|
+
({ item: msg }) => {
|
|
5528
|
+
const showInlineChoices = inlineChoiceSet?.messageId === msg.id;
|
|
5529
|
+
return (
|
|
5144
5530
|
<View style={styles.chatMessageBlock}>
|
|
5145
|
-
<ChatMessage
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5531
|
+
<ChatMessage message={msg} />
|
|
5532
|
+
{showInlineChoices ? (
|
|
5533
|
+
<View style={styles.inlineChoiceOptions}>
|
|
5534
|
+
{inlineChoiceSet.options.map((option, index) => (
|
|
5535
|
+
<Pressable
|
|
5536
|
+
key={`${msg.id}-${index}-${option.label}`}
|
|
5537
|
+
style={({ pressed }) => [
|
|
5538
|
+
styles.inlineChoiceOptionButton,
|
|
5539
|
+
pressed && styles.inlineChoiceOptionButtonPressed,
|
|
5540
|
+
]}
|
|
5541
|
+
onPress={() => onInlineOptionSelect(option.label)}
|
|
5542
|
+
>
|
|
5543
|
+
<View style={styles.inlineChoiceOptionRow}>
|
|
5544
|
+
<Text style={styles.inlineChoiceOptionIndex}>{`${String(index + 1)}.`}</Text>
|
|
5545
|
+
<Text style={styles.inlineChoiceOptionLabel}>{option.label}</Text>
|
|
5546
|
+
</View>
|
|
5547
|
+
{option.description.trim() ? (
|
|
5548
|
+
<Text style={styles.inlineChoiceOptionDescription}>
|
|
5549
|
+
{option.description}
|
|
5550
|
+
</Text>
|
|
5551
|
+
) : null}
|
|
5552
|
+
</Pressable>
|
|
5553
|
+
))}
|
|
5554
|
+
<Text style={styles.inlineChoiceHint}>
|
|
5555
|
+
Tap an option to fill the reply box.
|
|
5556
|
+
</Text>
|
|
5557
|
+
</View>
|
|
5558
|
+
) : null}
|
|
5153
5559
|
</View>
|
|
5154
|
-
)
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5560
|
+
);
|
|
5561
|
+
},
|
|
5562
|
+
[inlineChoiceSet, onInlineOptionSelect]
|
|
5563
|
+
);
|
|
5564
|
+
|
|
5565
|
+
return (
|
|
5566
|
+
<View style={styles.messageListShell}>
|
|
5567
|
+
<FlatList
|
|
5568
|
+
key={chat.id}
|
|
5569
|
+
ref={scrollRef}
|
|
5570
|
+
data={visibleMessages}
|
|
5571
|
+
keyExtractor={keyExtractor}
|
|
5572
|
+
renderItem={renderMessageItem}
|
|
5573
|
+
style={styles.messageList}
|
|
5574
|
+
contentContainerStyle={messageListContentStyle}
|
|
5575
|
+
showsVerticalScrollIndicator={false}
|
|
5576
|
+
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
|
|
5577
|
+
keyboardShouldPersistTaps="handled"
|
|
5578
|
+
onScrollBeginDrag={Keyboard.dismiss}
|
|
5579
|
+
onScroll={handleScroll}
|
|
5580
|
+
scrollEventThrottle={16}
|
|
5581
|
+
onContentSizeChange={() => {
|
|
5582
|
+
if (shouldStickToBottomRef.current) {
|
|
5583
|
+
onAutoScroll(false);
|
|
5584
|
+
}
|
|
5585
|
+
}}
|
|
5586
|
+
initialNumToRender={isLargeChat ? aggressiveRenderBatchSize : 16}
|
|
5587
|
+
maxToRenderPerBatch={isLargeChat ? aggressiveRenderBatchSize : 12}
|
|
5588
|
+
updateCellsBatchingPeriod={isLargeChat ? 0 : undefined}
|
|
5589
|
+
windowSize={isLargeChat ? 21 : 11}
|
|
5590
|
+
removeClippedSubviews={isLargeChat ? false : Platform.OS === 'android'}
|
|
5591
|
+
ListHeaderComponent={activePlan ? <PlanCard plan={activePlan} /> : null}
|
|
5592
|
+
ListFooterComponent={
|
|
5593
|
+
<>
|
|
5594
|
+
{liveTimelineText ? (
|
|
5595
|
+
<View style={styles.chatMessageBlock}>
|
|
5596
|
+
<ChatMessage
|
|
5597
|
+
message={{
|
|
5598
|
+
id: `live-timeline-${chat.id}`,
|
|
5599
|
+
role: 'system',
|
|
5600
|
+
content: liveTimelineText,
|
|
5601
|
+
createdAt: new Date().toISOString(),
|
|
5602
|
+
}}
|
|
5177
5603
|
/>
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5604
|
+
</View>
|
|
5605
|
+
) : null}
|
|
5606
|
+
{streamingPreviewText ? (
|
|
5607
|
+
<Text style={styles.streamingText} numberOfLines={4}>
|
|
5608
|
+
{streamingPreviewText}
|
|
5609
|
+
</Text>
|
|
5610
|
+
) : null}
|
|
5611
|
+
{shouldShowToolPanel ? (
|
|
5612
|
+
<View style={[styles.toolPanel, { maxHeight: toolPanelMaxHeight }]}>
|
|
5613
|
+
<ScrollView
|
|
5614
|
+
nestedScrollEnabled
|
|
5615
|
+
showsVerticalScrollIndicator={false}
|
|
5616
|
+
contentContainerStyle={styles.toolPanelContent}
|
|
5617
|
+
>
|
|
5618
|
+
{visibleToolBlocks.map((cmd) => {
|
|
5619
|
+
const tool = toToolBlockState(cmd);
|
|
5620
|
+
if (!tool) {
|
|
5621
|
+
return null;
|
|
5622
|
+
}
|
|
5623
|
+
return (
|
|
5624
|
+
<ToolBlock
|
|
5625
|
+
key={cmd.id}
|
|
5626
|
+
command={tool.command}
|
|
5627
|
+
status={tool.status}
|
|
5628
|
+
/>
|
|
5629
|
+
);
|
|
5630
|
+
})}
|
|
5631
|
+
</ScrollView>
|
|
5632
|
+
</View>
|
|
5633
|
+
) : null}
|
|
5634
|
+
{isStreaming && !streamingPreviewText && activeCommands.length === 0 ? (
|
|
5635
|
+
<TypingIndicator />
|
|
5636
|
+
) : null}
|
|
5637
|
+
</>
|
|
5638
|
+
}
|
|
5639
|
+
/>
|
|
5640
|
+
</View>
|
|
5185
5641
|
);
|
|
5186
5642
|
}
|
|
5187
5643
|
|
|
@@ -5450,10 +5906,10 @@ function findInlineChoiceSet(messages: ChatTranscriptMessage[]): {
|
|
|
5450
5906
|
continue;
|
|
5451
5907
|
}
|
|
5452
5908
|
|
|
5453
|
-
const cueSource =
|
|
5909
|
+
const cueSource = parsed.question.trim();
|
|
5454
5910
|
const hasCue =
|
|
5455
5911
|
cueSource.includes('?') ||
|
|
5456
|
-
|
|
5912
|
+
INLINE_CHOICE_CUE_PATTERNS.some((pattern) => pattern.test(cueSource));
|
|
5457
5913
|
if (!hasCue) {
|
|
5458
5914
|
continue;
|
|
5459
5915
|
}
|
|
@@ -6135,6 +6591,110 @@ function isCodexRunHeartbeatEvent(codexEventType: string): boolean {
|
|
|
6135
6591
|
return CODEX_RUN_HEARTBEAT_EVENT_TYPES.has(codexEventType);
|
|
6136
6592
|
}
|
|
6137
6593
|
|
|
6594
|
+
function normalizeExternalStatusHint(value: string | null | undefined): string | null {
|
|
6595
|
+
if (!value) {
|
|
6596
|
+
return null;
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
6600
|
+
return normalized.length > 0 ? normalized : null;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
function extractNotificationThreadId(
|
|
6604
|
+
params: Record<string, unknown> | null,
|
|
6605
|
+
msgArg?: Record<string, unknown> | null
|
|
6606
|
+
): string | null {
|
|
6607
|
+
if (!params && !msgArg) {
|
|
6608
|
+
return null;
|
|
6609
|
+
}
|
|
6610
|
+
|
|
6611
|
+
const msg = msgArg ?? toRecord(params?.msg);
|
|
6612
|
+
const threadRecord =
|
|
6613
|
+
toRecord(params?.thread) ??
|
|
6614
|
+
toRecord(params?.threadState) ??
|
|
6615
|
+
toRecord(params?.thread_state) ??
|
|
6616
|
+
toRecord(msg?.thread);
|
|
6617
|
+
const turnRecord = toRecord(params?.turn) ?? toRecord(msg?.turn);
|
|
6618
|
+
const sourceRecord = toRecord(params?.source) ?? toRecord(msg?.source);
|
|
6619
|
+
const subagentThreadSpawnRecord = toRecord(
|
|
6620
|
+
toRecord(sourceRecord?.subagent)?.thread_spawn
|
|
6621
|
+
);
|
|
6622
|
+
|
|
6623
|
+
return (
|
|
6624
|
+
readString(msg?.thread_id) ??
|
|
6625
|
+
readString(msg?.threadId) ??
|
|
6626
|
+
readString(msg?.conversation_id) ??
|
|
6627
|
+
readString(msg?.conversationId) ??
|
|
6628
|
+
readString(params?.thread_id) ??
|
|
6629
|
+
readString(params?.threadId) ??
|
|
6630
|
+
readString(params?.conversation_id) ??
|
|
6631
|
+
readString(params?.conversationId) ??
|
|
6632
|
+
readString(threadRecord?.id) ??
|
|
6633
|
+
readString(threadRecord?.thread_id) ??
|
|
6634
|
+
readString(threadRecord?.threadId) ??
|
|
6635
|
+
readString(threadRecord?.conversation_id) ??
|
|
6636
|
+
readString(threadRecord?.conversationId) ??
|
|
6637
|
+
readString(turnRecord?.thread_id) ??
|
|
6638
|
+
readString(turnRecord?.threadId) ??
|
|
6639
|
+
readString(sourceRecord?.thread_id) ??
|
|
6640
|
+
readString(sourceRecord?.threadId) ??
|
|
6641
|
+
readString(sourceRecord?.conversation_id) ??
|
|
6642
|
+
readString(sourceRecord?.conversationId) ??
|
|
6643
|
+
readString(sourceRecord?.parent_thread_id) ??
|
|
6644
|
+
readString(sourceRecord?.parentThreadId) ??
|
|
6645
|
+
readString(subagentThreadSpawnRecord?.parent_thread_id) ??
|
|
6646
|
+
null
|
|
6647
|
+
);
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
function extractExternalStatusHint(
|
|
6651
|
+
params: Record<string, unknown> | null
|
|
6652
|
+
): string | null {
|
|
6653
|
+
if (!params) {
|
|
6654
|
+
return null;
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
const directCandidates: unknown[] = [
|
|
6658
|
+
params.status,
|
|
6659
|
+
params.threadStatus,
|
|
6660
|
+
params.thread_status,
|
|
6661
|
+
params.state,
|
|
6662
|
+
params.phase,
|
|
6663
|
+
];
|
|
6664
|
+
for (const candidate of directCandidates) {
|
|
6665
|
+
const direct = normalizeExternalStatusHint(readString(candidate));
|
|
6666
|
+
if (direct) {
|
|
6667
|
+
return direct;
|
|
6668
|
+
}
|
|
6669
|
+
|
|
6670
|
+
const candidateRecord = toRecord(candidate);
|
|
6671
|
+
const typed = normalizeExternalStatusHint(
|
|
6672
|
+
readString(candidateRecord?.type) ??
|
|
6673
|
+
readString(candidateRecord?.status) ??
|
|
6674
|
+
readString(candidateRecord?.state) ??
|
|
6675
|
+
readString(candidateRecord?.phase)
|
|
6676
|
+
);
|
|
6677
|
+
if (typed) {
|
|
6678
|
+
return typed;
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
const threadRecord =
|
|
6683
|
+
toRecord(params.thread) ?? toRecord(params.threadState) ?? toRecord(params.thread_state);
|
|
6684
|
+
if (!threadRecord) {
|
|
6685
|
+
return null;
|
|
6686
|
+
}
|
|
6687
|
+
|
|
6688
|
+
const nestedThreadStatus = normalizeExternalStatusHint(
|
|
6689
|
+
readString(threadRecord.status) ??
|
|
6690
|
+
readString(toRecord(threadRecord.status)?.type) ??
|
|
6691
|
+
readString(threadRecord.state) ??
|
|
6692
|
+
readString(threadRecord.phase) ??
|
|
6693
|
+
readString(toRecord(threadRecord.lifecycle)?.status)
|
|
6694
|
+
);
|
|
6695
|
+
return nestedThreadStatus;
|
|
6696
|
+
}
|
|
6697
|
+
|
|
6138
6698
|
function isChatSummaryLikelyRunning(chat: ChatSummary): boolean {
|
|
6139
6699
|
return chat.status === 'running';
|
|
6140
6700
|
}
|
|
@@ -6162,6 +6722,70 @@ function isChatLikelyRunning(chat: Chat): boolean {
|
|
|
6162
6722
|
return Date.now() - updatedAtMs < LIKELY_RUNNING_RECENT_UPDATE_MS;
|
|
6163
6723
|
}
|
|
6164
6724
|
|
|
6725
|
+
function hasRecentUnansweredUserTurn(chat: Chat): boolean {
|
|
6726
|
+
let lastUserIndex = -1;
|
|
6727
|
+
for (let index = chat.messages.length - 1; index >= 0; index -= 1) {
|
|
6728
|
+
if (chat.messages[index].role === 'user') {
|
|
6729
|
+
lastUserIndex = index;
|
|
6730
|
+
break;
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6734
|
+
if (lastUserIndex < 0) {
|
|
6735
|
+
return false;
|
|
6736
|
+
}
|
|
6737
|
+
|
|
6738
|
+
for (let index = lastUserIndex + 1; index < chat.messages.length; index += 1) {
|
|
6739
|
+
if (chat.messages[index].role === 'assistant') {
|
|
6740
|
+
return false;
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
const lastUser = chat.messages[lastUserIndex];
|
|
6745
|
+
const userCreatedAtMs = Date.parse(lastUser.createdAt);
|
|
6746
|
+
if (!Number.isFinite(userCreatedAtMs)) {
|
|
6747
|
+
return false;
|
|
6748
|
+
}
|
|
6749
|
+
|
|
6750
|
+
return Date.now() - userCreatedAtMs < UNANSWERED_USER_RUNNING_TTL_MS;
|
|
6751
|
+
}
|
|
6752
|
+
|
|
6753
|
+
function didAssistantMessageProgress(previous: Chat | null, next: Chat): boolean {
|
|
6754
|
+
if (!previous || previous.id !== next.id) {
|
|
6755
|
+
return false;
|
|
6756
|
+
}
|
|
6757
|
+
|
|
6758
|
+
const previousLatestAssistant = latestAssistantMessage(previous.messages);
|
|
6759
|
+
const nextLatestAssistant = latestAssistantMessage(next.messages);
|
|
6760
|
+
|
|
6761
|
+
if (!nextLatestAssistant) {
|
|
6762
|
+
return false;
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
if (!previousLatestAssistant) {
|
|
6766
|
+
return nextLatestAssistant.content.trim().length > 0;
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
if (nextLatestAssistant.id === previousLatestAssistant.id) {
|
|
6770
|
+
return nextLatestAssistant.content.length > previousLatestAssistant.content.length;
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
return (
|
|
6774
|
+
next.messages.length > previous.messages.length &&
|
|
6775
|
+
nextLatestAssistant.content.trim().length > 0
|
|
6776
|
+
);
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
function latestAssistantMessage(messages: ChatTranscriptMessage[]): ChatTranscriptMessage | null {
|
|
6780
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
6781
|
+
const message = messages[index];
|
|
6782
|
+
if (message.role === 'assistant') {
|
|
6783
|
+
return message;
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
return null;
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6165
6789
|
function extractFirstBoldSnippet(
|
|
6166
6790
|
value: string | null | undefined,
|
|
6167
6791
|
maxLength = 56
|
|
@@ -6248,6 +6872,15 @@ const styles = StyleSheet.create({
|
|
|
6248
6872
|
composerContainer: {
|
|
6249
6873
|
backgroundColor: colors.bgMain,
|
|
6250
6874
|
},
|
|
6875
|
+
composerContainerResting: {
|
|
6876
|
+
marginBottom: spacing.xs,
|
|
6877
|
+
},
|
|
6878
|
+
activityOverlay: {
|
|
6879
|
+
position: 'absolute',
|
|
6880
|
+
left: 0,
|
|
6881
|
+
right: 0,
|
|
6882
|
+
zIndex: 3,
|
|
6883
|
+
},
|
|
6251
6884
|
sessionMetaRow: {
|
|
6252
6885
|
flexDirection: 'row',
|
|
6253
6886
|
alignItems: 'center',
|
|
@@ -6778,10 +7411,15 @@ const styles = StyleSheet.create({
|
|
|
6778
7411
|
},
|
|
6779
7412
|
|
|
6780
7413
|
// Chat
|
|
7414
|
+
messageListShell: {
|
|
7415
|
+
flex: 1,
|
|
7416
|
+
},
|
|
6781
7417
|
messageList: {
|
|
6782
7418
|
flex: 1,
|
|
6783
7419
|
},
|
|
6784
7420
|
messageListContent: {
|
|
7421
|
+
flexGrow: 1,
|
|
7422
|
+
justifyContent: 'flex-end',
|
|
6785
7423
|
padding: spacing.lg,
|
|
6786
7424
|
paddingTop: spacing.lg,
|
|
6787
7425
|
paddingBottom: spacing.xl,
|