clawdex-mobile 2.0.1 → 3.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.
Files changed (71) hide show
  1. package/.github/workflows/pages.yml +41 -0
  2. package/AGENTS.md +263 -110
  3. package/README.md +1 -1
  4. package/apps/mobile/.env.example +2 -2
  5. package/apps/mobile/App.tsx +175 -14
  6. package/apps/mobile/app.json +27 -9
  7. package/apps/mobile/eas.json +14 -4
  8. package/apps/mobile/package.json +13 -13
  9. package/apps/mobile/src/api/__tests__/chatMapping.test.ts +219 -0
  10. package/apps/mobile/src/api/__tests__/client.test.ts +579 -6
  11. package/apps/mobile/src/api/__tests__/ws.test.ts +27 -0
  12. package/apps/mobile/src/api/account.ts +47 -0
  13. package/apps/mobile/src/api/chatMapping.ts +435 -18
  14. package/apps/mobile/src/api/client.ts +296 -36
  15. package/apps/mobile/src/api/rateLimits.ts +143 -0
  16. package/apps/mobile/src/api/types.ts +106 -0
  17. package/apps/mobile/src/api/ws.ts +10 -1
  18. package/apps/mobile/src/components/ChatHeader.tsx +12 -12
  19. package/apps/mobile/src/components/ChatInput.tsx +154 -88
  20. package/apps/mobile/src/components/ChatMessage.tsx +548 -93
  21. package/apps/mobile/src/components/ComposerUsageLimits.tsx +167 -0
  22. package/apps/mobile/src/components/SelectionSheet.tsx +466 -0
  23. package/apps/mobile/src/components/ToolBlock.tsx +17 -15
  24. package/apps/mobile/src/components/VoiceRecordingWaveform.tsx +181 -0
  25. package/apps/mobile/src/components/WorkspacePickerModal.tsx +572 -0
  26. package/apps/mobile/src/components/__tests__/chat-input-layout.test.ts +35 -0
  27. package/apps/mobile/src/components/__tests__/chatImageSource.test.ts +44 -0
  28. package/apps/mobile/src/components/__tests__/composerUsageLimits.test.ts +138 -0
  29. package/apps/mobile/src/components/__tests__/voiceWaveform.test.ts +31 -0
  30. package/apps/mobile/src/components/chat-input-layout.ts +59 -0
  31. package/apps/mobile/src/components/chatImageSource.ts +86 -0
  32. package/apps/mobile/src/components/usageLimitBadges.ts +109 -0
  33. package/apps/mobile/src/components/voiceWaveform.ts +46 -0
  34. package/apps/mobile/src/config.ts +9 -2
  35. package/apps/mobile/src/hooks/useVoiceRecorder.ts +8 -1
  36. package/apps/mobile/src/navigation/DrawerContent.tsx +607 -457
  37. package/apps/mobile/src/navigation/__tests__/chatThreadTree.test.ts +89 -0
  38. package/apps/mobile/src/navigation/__tests__/drawerChats.test.ts +65 -0
  39. package/apps/mobile/src/navigation/chatThreadTree.ts +191 -0
  40. package/apps/mobile/src/navigation/drawerChats.ts +9 -0
  41. package/apps/mobile/src/screens/GitScreen.tsx +2 -0
  42. package/apps/mobile/src/screens/MainScreen.tsx +4244 -1237
  43. package/apps/mobile/src/screens/OnboardingScreen.tsx +2 -0
  44. package/apps/mobile/src/screens/SettingsScreen.tsx +256 -226
  45. package/apps/mobile/src/screens/TerminalScreen.tsx +2 -5
  46. package/apps/mobile/src/screens/__tests__/agentThreadDisplay.test.ts +80 -0
  47. package/apps/mobile/src/screens/__tests__/agentThreads.test.ts +170 -0
  48. package/apps/mobile/src/screens/__tests__/planCardState.test.ts +88 -0
  49. package/apps/mobile/src/screens/__tests__/subAgentTranscript.test.ts +102 -0
  50. package/apps/mobile/src/screens/__tests__/transcriptMessages.test.ts +97 -0
  51. package/apps/mobile/src/screens/agentThreadDisplay.ts +261 -0
  52. package/apps/mobile/src/screens/agentThreads.ts +167 -0
  53. package/apps/mobile/src/screens/planCardState.ts +40 -0
  54. package/apps/mobile/src/screens/subAgentTranscript.ts +149 -0
  55. package/apps/mobile/src/screens/transcriptMessages.ts +102 -0
  56. package/apps/mobile/src/theme.ts +6 -12
  57. package/docs/codex-app-server-cli-gap-tracker.md +14 -5
  58. package/docs/privacy-policy.md +54 -0
  59. package/docs/setup-and-operations.md +4 -3
  60. package/docs/terms-of-service.md +33 -0
  61. package/package.json +3 -3
  62. package/services/mac-bridge/package.json +6 -6
  63. package/services/rust-bridge/Cargo.lock +56 -47
  64. package/services/rust-bridge/Cargo.toml +1 -1
  65. package/services/rust-bridge/package.json +1 -1
  66. package/services/rust-bridge/src/main.rs +507 -9
  67. package/site/index.html +54 -0
  68. package/site/privacy/index.html +80 -0
  69. package/site/styles.css +135 -0
  70. package/site/support/index.html +51 -0
  71. package/site/terms/index.html +68 -0
@@ -3,15 +3,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import {
4
4
  ActivityIndicator,
5
5
  AppState,
6
- Modal,
7
6
  Pressable,
8
7
  RefreshControl,
9
8
  SectionList,
10
- ScrollView,
11
- type StyleProp,
12
9
  StyleSheet,
13
10
  Text,
14
- type ViewStyle,
15
11
  View,
16
12
  } from 'react-native';
17
13
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -19,6 +15,12 @@ import type { HostBridgeApiClient } from '../api/client';
19
15
  import type { ChatSummary, RpcNotification } from '../api/types';
20
16
  import type { HostBridgeWsClient } from '../api/ws';
21
17
  import { BrandMark } from '../components/BrandMark';
18
+ import { filterDrawerChats } from './drawerChats';
19
+ import { describeAgentThreadSource } from '../screens/agentThreads';
20
+ import {
21
+ buildChatWorkspaceSections,
22
+ type ChatWorkspaceSection,
23
+ } from './chatThreadTree';
22
24
  import { colors, spacing, typography } from '../theme';
23
25
 
24
26
  type Screen = 'Main' | 'Settings' | 'Privacy' | 'Terms';
@@ -27,20 +29,11 @@ interface DrawerContentProps {
27
29
  api: HostBridgeApiClient;
28
30
  ws: HostBridgeWsClient;
29
31
  selectedChatId: string | null;
30
- selectedDefaultCwd: string | null;
31
- onSelectDefaultCwd: (cwd: string | null) => void;
32
32
  onSelectChat: (id: string) => void;
33
33
  onNewChat: () => void;
34
34
  onNavigate: (screen: Screen) => void;
35
35
  }
36
36
 
37
- interface ChatWorkspaceSection {
38
- key: string;
39
- title: string;
40
- subtitle?: string;
41
- data: ChatSummary[];
42
- }
43
-
44
37
  const RUN_HEARTBEAT_STALE_MS = 20_000;
45
38
  const DRAWER_REFRESH_CONNECTED_MS = 10_000;
46
39
  const DRAWER_REFRESH_DISCONNECTED_MS = 5_000;
@@ -65,8 +58,6 @@ export function DrawerContent({
65
58
  api,
66
59
  ws,
67
60
  selectedChatId,
68
- selectedDefaultCwd,
69
- onSelectDefaultCwd,
70
61
  onSelectChat,
71
62
  onNewChat,
72
63
  onNavigate,
@@ -74,14 +65,12 @@ export function DrawerContent({
74
65
  const [chats, setChats] = useState<ChatSummary[]>([]);
75
66
  const [loading, setLoading] = useState(true);
76
67
  const [refreshing, setRefreshing] = useState(false);
77
- const [workspacePickerOpen, setWorkspacePickerOpen] = useState(false);
78
68
  const [collapsedWorkspaceKeys, setCollapsedWorkspaceKeys] = useState<Set<string>>(new Set());
79
69
  const [runHeartbeatAtByThread, setRunHeartbeatAtByThread] = useState<Record<string, number>>({});
80
70
  const [wsConnected, setWsConnected] = useState(ws.isConnected);
81
71
  const hasAppliedInitialCollapseRef = useRef(false);
82
72
  const chatSectionsRef = useRef<ChatWorkspaceSection[]>([]);
83
- const workspaceOptions = useMemo(() => listWorkspaces(chats), [chats]);
84
- const chatSections = useMemo(() => buildWorkspaceSections(chats), [chats]);
73
+ const chatSections = useMemo(() => buildChatWorkspaceSections(chats), [chats]);
85
74
  const visibleChatSections = useMemo(
86
75
  () =>
87
76
  chatSections.map((section) =>
@@ -94,8 +83,14 @@ export function DrawerContent({
94
83
  ),
95
84
  [chatSections, collapsedWorkspaceKeys]
96
85
  );
97
- const defaultWorkspaceLabel =
98
- normalizeCwd(selectedDefaultCwd) ?? 'Bridge default workspace';
86
+ const runningChatCount = useMemo(() => {
87
+ const now = Date.now();
88
+ return chats.reduce((count, chat) => {
89
+ const runningFromHeartbeat =
90
+ (runHeartbeatAtByThread[chat.id] ?? 0) > now - RUN_HEARTBEAT_STALE_MS;
91
+ return count + (chat.status === 'running' || runningFromHeartbeat ? 1 : 0);
92
+ }, 0);
93
+ }, [chats, runHeartbeatAtByThread]);
99
94
 
100
95
  const loadChats = useCallback(async (showRefresh = false) => {
101
96
  if (showRefresh) {
@@ -103,8 +98,26 @@ export function DrawerContent({
103
98
  }
104
99
 
105
100
  try {
106
- const data = await api.listChats();
107
- const dedupedChats = dedupeChatsById(data);
101
+ const listedChats = await api.listChats();
102
+ const listedChatIds = new Set(listedChats.map((chat) => chat.id));
103
+ let loadedChats: ChatSummary[] = [];
104
+
105
+ try {
106
+ const loadedIds = await api.listLoadedChatIds();
107
+ const missingIds = loadedIds.filter((threadId) => !listedChatIds.has(threadId));
108
+ if (missingIds.length > 0) {
109
+ const loadedResults = await Promise.allSettled(
110
+ missingIds.map((threadId) => api.getChatSummary(threadId))
111
+ );
112
+ loadedChats = loadedResults.flatMap((result) =>
113
+ result.status === 'fulfilled' ? [result.value] : []
114
+ );
115
+ }
116
+ } catch {
117
+ // Keep the drawer usable if loaded-thread hydration fails.
118
+ }
119
+
120
+ const dedupedChats = dedupeChatsById(filterDrawerChats([...listedChats, ...loadedChats]));
108
121
  setChats(sortChats(dedupedChats));
109
122
  const activeChatIds = new Set(dedupedChats.map((chat) => chat.id));
110
123
  setRunHeartbeatAtByThread((prev) => {
@@ -284,56 +297,112 @@ export function DrawerContent({
284
297
  <View style={styles.container}>
285
298
  <SafeAreaView style={styles.safeArea}>
286
299
  <View style={styles.mainContent}>
287
- <View style={styles.brandRow}>
288
- <BrandMark size={20} />
289
- <Text style={styles.brandText}>Clawdex</Text>
290
- </View>
300
+ <View style={styles.topDeck}>
301
+ <View style={styles.heroCard}>
302
+ <View style={styles.heroHeaderRow}>
303
+ <View style={styles.brandBadge}>
304
+ <BrandMark size={18} />
305
+ </View>
306
+ <View style={styles.heroCopy}>
307
+ <Text style={styles.heroTitle}>Clawdex</Text>
308
+ <Text style={styles.heroSubtitle} numberOfLines={1}>
309
+ Threads, terminal, and git in a focused mobile control deck.
310
+ </Text>
311
+ </View>
312
+ <View
313
+ style={[
314
+ styles.connectionBadge,
315
+ wsConnected
316
+ ? styles.connectionBadgeConnected
317
+ : styles.connectionBadgeDisconnected,
318
+ ]}
319
+ >
320
+ <View
321
+ style={[
322
+ styles.connectionDot,
323
+ wsConnected
324
+ ? styles.connectionDotConnected
325
+ : styles.connectionDotDisconnected,
326
+ ]}
327
+ />
328
+ <Text
329
+ style={[
330
+ styles.connectionText,
331
+ wsConnected
332
+ ? styles.connectionTextConnected
333
+ : styles.connectionTextDisconnected,
334
+ ]}
335
+ >
336
+ {wsConnected ? 'Live' : 'Offline'}
337
+ </Text>
338
+ </View>
339
+ </View>
291
340
 
292
- {/* New Chat button */}
293
- <View style={styles.header}>
294
- <Pressable
295
- style={({ pressed }) => [
296
- styles.navItem,
297
- styles.newChatBtn,
298
- pressed && styles.navItemPressed,
299
- ]}
300
- onPress={onNewChat}
301
- >
302
- <Ionicons name="add" size={16} color={colors.textPrimary} />
303
- <Text style={styles.newChatText}>New chat</Text>
304
- </Pressable>
305
- </View>
341
+ <View style={styles.heroStatsRow}>
342
+ <View style={styles.heroStat}>
343
+ <Text style={styles.heroStatValue}>{formatCompactCount(chats.length)}</Text>
344
+ <Text style={styles.heroStatLabel}>Chats</Text>
345
+ </View>
346
+ <View style={styles.heroStatsDivider} />
347
+ <View style={styles.heroStat}>
348
+ <Text style={styles.heroStatValue}>
349
+ {formatCompactCount(runningChatCount)}
350
+ </Text>
351
+ <Text style={styles.heroStatLabel}>Running</Text>
352
+ </View>
353
+ <View style={styles.heroStatsDivider} />
354
+ <View style={styles.heroStat}>
355
+ <Text style={styles.heroStatValue}>
356
+ {formatCompactCount(chatSections.length)}
357
+ </Text>
358
+ <Text style={styles.heroStatLabel}>Spaces</Text>
359
+ </View>
360
+ </View>
361
+ </View>
306
362
 
307
- <View style={styles.workspaceSection}>
308
- <Text style={styles.sectionTitle}>Start Directory</Text>
309
- <Pressable
310
- style={({ pressed }) => [
311
- styles.workspacePicker,
312
- pressed && styles.workspacePickerPressed,
313
- ]}
314
- onPress={() => setWorkspacePickerOpen(true)}
315
- >
316
- <Ionicons name="folder-open-outline" size={16} color={colors.textMuted} />
317
- <Text style={styles.workspacePickerText} numberOfLines={1}>
318
- {defaultWorkspaceLabel}
319
- </Text>
320
- <Ionicons name="chevron-down" size={14} color={colors.textMuted} />
321
- </Pressable>
363
+ <View style={styles.actionRow}>
364
+ <Pressable
365
+ style={({ pressed }) => [
366
+ styles.primaryActionButton,
367
+ pressed && styles.primaryActionButtonPressed,
368
+ ]}
369
+ onPress={onNewChat}
370
+ >
371
+ <Ionicons name="add" size={18} color={colors.black} />
372
+ <Text style={styles.primaryActionText}>New chat</Text>
373
+ </Pressable>
374
+ </View>
322
375
  </View>
323
376
 
324
- {/* Chats section */}
325
377
  <View style={styles.sectionHeader}>
326
378
  <Text style={styles.sectionTitle}>Chats</Text>
379
+ <View style={styles.sectionCountBadge}>
380
+ <Text style={styles.sectionCountText}>
381
+ {formatCompactCount(chats.length)}
382
+ </Text>
383
+ </View>
327
384
  </View>
328
385
 
329
386
  {loading ? (
330
- <ActivityIndicator color={colors.textMuted} style={styles.loader} />
387
+ <View style={styles.emptyStateCard}>
388
+ <ActivityIndicator color={colors.textMuted} style={styles.loader} />
389
+ <Text style={styles.emptyTitle}>Loading chats</Text>
390
+ <Text style={styles.emptyHint}>Syncing recent threads from your bridge.</Text>
391
+ </View>
331
392
  ) : chatSections.length === 0 ? (
332
- <Text style={styles.emptyText}>No chats yet</Text>
393
+ <View style={styles.emptyStateCard}>
394
+ <View style={styles.emptyStateIconWrap}>
395
+ <Ionicons name="sparkles-outline" size={18} color={colors.textPrimary} />
396
+ </View>
397
+ <Text style={styles.emptyTitle}>No chats yet</Text>
398
+ <Text style={styles.emptyHint}>
399
+ Start a new chat and it will show up here with live activity.
400
+ </Text>
401
+ </View>
333
402
  ) : (
334
403
  <SectionList
335
404
  sections={visibleChatSections}
336
- keyExtractor={(item) => item.id}
405
+ keyExtractor={(item) => item.chat.id}
337
406
  style={styles.list}
338
407
  contentContainerStyle={styles.listContent}
339
408
  showsVerticalScrollIndicator={false}
@@ -353,7 +422,9 @@ export function DrawerContent({
353
422
  <Pressable
354
423
  style={({ pressed }) => [
355
424
  styles.workspaceGroupHeader,
356
- collapsed ? styles.workspaceGroupHeaderCollapsed : styles.workspaceGroupHeaderExpanded,
425
+ collapsed
426
+ ? styles.workspaceGroupHeaderCollapsed
427
+ : styles.workspaceGroupHeaderExpanded,
357
428
  pressed && styles.workspaceGroupHeaderPressed,
358
429
  ]}
359
430
  onPress={() => toggleWorkspaceSection(section.key)}
@@ -369,6 +440,11 @@ export function DrawerContent({
369
440
  </Text>
370
441
  ) : null}
371
442
  </View>
443
+ <View style={styles.workspaceGroupCountBadge}>
444
+ <Text style={styles.workspaceGroupCountText}>
445
+ {formatCompactCount(section.itemCount)}
446
+ </Text>
447
+ </View>
372
448
  <View style={styles.workspaceGroupHeaderMeta}>
373
449
  <Ionicons
374
450
  name={collapsed ? 'chevron-forward' : 'chevron-down'}
@@ -381,33 +457,99 @@ export function DrawerContent({
381
457
  );
382
458
  }}
383
459
  renderItem={({ item, index, section }) => {
384
- const isSelected = item.id === selectedChatId;
460
+ const chat = item.chat;
461
+ const isSelected = chat.id === selectedChatId;
385
462
  const isLast = index === section.data.length - 1;
386
463
  const isRunningFromHeartbeat =
387
- (runHeartbeatAtByThread[item.id] ?? 0) > Date.now() - RUN_HEARTBEAT_STALE_MS;
388
- const isRunning = item.status === 'running' || isRunningFromHeartbeat;
464
+ (runHeartbeatAtByThread[chat.id] ?? 0) > Date.now() - RUN_HEARTBEAT_STALE_MS;
465
+ const isRunning = chat.status === 'running' || isRunningFromHeartbeat;
466
+ const isSubAgent = item.indentLevel > 0 || Boolean(chat.parentThreadId);
467
+ const previewText = isSubAgent
468
+ ? `${describeAgentThreadSource(chat, item.rootThreadId)} • ${formatChatPreview(chat)}`
469
+ : formatChatPreview(chat);
389
470
  return (
390
471
  <Pressable
391
472
  style={({ pressed }) => [
392
473
  styles.chatItem,
393
- isLast && styles.chatItemLast,
474
+ isSubAgent && styles.chatItemSubAgent,
475
+ isSubAgent && { marginLeft: Math.min(item.indentLevel, 4) * 18 },
394
476
  isSelected && styles.chatItemSelected,
395
477
  pressed && styles.chatItemPressed,
478
+ isLast && styles.chatItemLast,
396
479
  ]}
397
- onPress={() => onSelectChat(item.id)}
480
+ onPress={() => onSelectChat(chat.id)}
398
481
  >
399
- <Text style={[styles.chatTitle, isSelected && styles.chatTitleSelected]} numberOfLines={1}>
400
- {item.title || 'Untitled'}
401
- </Text>
402
- <View style={styles.chatMeta}>
403
- {isRunning ? (
404
- <ActivityIndicator
405
- size="small"
406
- color={colors.statusRunning}
407
- style={styles.chatSpinner}
408
- />
409
- ) : null}
410
- <Text style={styles.chatAge}>{relativeTime(item.updatedAt)}</Text>
482
+ <View
483
+ style={[
484
+ styles.chatItemAccent,
485
+ isSubAgent && styles.chatItemAccentSubAgent,
486
+ isSelected && styles.chatItemAccentSelected,
487
+ isRunning && styles.chatItemAccentRunning,
488
+ chat.status === 'error' && styles.chatItemAccentError,
489
+ ]}
490
+ />
491
+ <View style={styles.chatItemContent}>
492
+ <View style={styles.chatItemTopRow}>
493
+ {isSubAgent ? (
494
+ <Ionicons
495
+ name="git-branch-outline"
496
+ size={12}
497
+ color="#F5A524"
498
+ style={styles.chatSubAgentIcon}
499
+ />
500
+ ) : null}
501
+ <Text
502
+ style={[
503
+ styles.chatTitle,
504
+ isSubAgent && styles.chatTitleSubAgent,
505
+ isSelected && styles.chatTitleSelected,
506
+ ]}
507
+ numberOfLines={1}
508
+ >
509
+ {chat.title || 'Untitled'}
510
+ </Text>
511
+ <Text
512
+ style={[styles.chatAge, isSelected && styles.chatAgeSelected]}
513
+ >
514
+ {relativeTime(chat.updatedAt)}
515
+ </Text>
516
+ </View>
517
+ <View style={styles.chatItemBottomRow}>
518
+ <Text
519
+ style={[
520
+ styles.chatPreview,
521
+ isSubAgent && styles.chatPreviewSubAgent,
522
+ isSelected && styles.chatPreviewSelected,
523
+ ]}
524
+ numberOfLines={1}
525
+ >
526
+ {previewText}
527
+ </Text>
528
+ {isRunning ? (
529
+ <View style={styles.chatMeta}>
530
+ <View style={[styles.statusPill, styles.statusPillRunning]}>
531
+ <View
532
+ style={[styles.statusPillDot, styles.statusPillDotRunning]}
533
+ />
534
+ <Text
535
+ style={[styles.statusPillText, styles.statusPillTextRunning]}
536
+ >
537
+ Live
538
+ </Text>
539
+ </View>
540
+ </View>
541
+ ) : chat.status === 'error' ? (
542
+ <View style={styles.chatMeta}>
543
+ <View style={[styles.statusPill, styles.statusPillError]}>
544
+ <Text
545
+ style={[styles.statusPillText, styles.statusPillTextError]}
546
+ >
547
+ Error
548
+ </Text>
549
+ </View>
550
+ </View>
551
+ ) : null}
552
+ </View>
411
553
  </View>
412
554
  </Pressable>
413
555
  );
@@ -417,124 +559,24 @@ export function DrawerContent({
417
559
  </View>
418
560
 
419
561
  <View style={styles.footer}>
420
- <NavItem
421
- icon="settings-outline"
422
- label="Settings"
562
+ <Pressable
563
+ accessibilityLabel="Open settings"
564
+ style={({ pressed }) => [
565
+ styles.footerSettingsButton,
566
+ pressed && styles.footerSettingsButtonPressed,
567
+ ]}
423
568
  onPress={() => onNavigate('Settings')}
424
- style={styles.settingsItem}
425
- pressableStyle={styles.footerNavItem}
426
- />
569
+ >
570
+ <Ionicons name="settings-outline" size={16} color={colors.textPrimary} />
571
+ <Text style={styles.footerSettingsText}>Settings</Text>
572
+ </Pressable>
427
573
  </View>
428
- </SafeAreaView>
429
574
 
430
- <Modal
431
- visible={workspacePickerOpen}
432
- transparent
433
- animationType="fade"
434
- onRequestClose={() => setWorkspacePickerOpen(false)}
435
- >
436
- <View style={styles.workspaceModalBackdrop}>
437
- <View style={styles.workspaceModalCard}>
438
- <Text style={styles.workspaceModalTitle}>Start directory for new chats</Text>
439
- <ScrollView
440
- style={styles.workspaceModalList}
441
- contentContainerStyle={styles.workspaceModalListContent}
442
- showsVerticalScrollIndicator={false}
443
- >
444
- <WorkspaceOption
445
- label="Bridge default workspace"
446
- selected={normalizeCwd(selectedDefaultCwd) === null}
447
- onPress={() => {
448
- onSelectDefaultCwd(null);
449
- setWorkspacePickerOpen(false);
450
- }}
451
- />
452
- {workspaceOptions.map((cwd) => (
453
- <WorkspaceOption
454
- key={cwd}
455
- label={cwd}
456
- selected={cwd === normalizeCwd(selectedDefaultCwd)}
457
- onPress={() => {
458
- onSelectDefaultCwd(cwd);
459
- setWorkspacePickerOpen(false);
460
- }}
461
- />
462
- ))}
463
- </ScrollView>
464
- <Pressable
465
- style={({ pressed }) => [
466
- styles.workspaceModalCloseBtn,
467
- pressed && styles.workspaceModalCloseBtnPressed,
468
- ]}
469
- onPress={() => setWorkspacePickerOpen(false)}
470
- >
471
- <Text style={styles.workspaceModalCloseText}>Close</Text>
472
- </Pressable>
473
- </View>
474
- </View>
475
- </Modal>
476
- </View>
477
- );
478
- }
479
-
480
- function NavItem({
481
- icon,
482
- label,
483
- onPress,
484
- style,
485
- pressableStyle,
486
- }: {
487
- icon: keyof typeof Ionicons.glyphMap;
488
- label: string;
489
- onPress: () => void;
490
- style?: StyleProp<ViewStyle>;
491
- pressableStyle?: StyleProp<ViewStyle>;
492
- }) {
493
- return (
494
- <View style={style}>
495
- <Pressable
496
- style={({ pressed }) => [
497
- styles.navItem,
498
- pressableStyle,
499
- pressed && styles.navItemPressed,
500
- ]}
501
- onPress={onPress}
502
- >
503
- <Ionicons name={icon} size={18} color={colors.textPrimary} />
504
- <Text style={styles.navLabel}>{label}</Text>
505
- </Pressable>
575
+ </SafeAreaView>
506
576
  </View>
507
577
  );
508
578
  }
509
579
 
510
- function WorkspaceOption({
511
- label,
512
- selected,
513
- onPress,
514
- }: {
515
- label: string;
516
- selected: boolean;
517
- onPress: () => void;
518
- }) {
519
- return (
520
- <Pressable
521
- style={({ pressed }) => [
522
- styles.workspaceOption,
523
- selected && styles.workspaceOptionSelected,
524
- pressed && styles.workspaceOptionPressed,
525
- ]}
526
- onPress={onPress}
527
- >
528
- <Text style={[styles.workspaceOptionText, selected && styles.workspaceOptionTextSelected]} numberOfLines={2}>
529
- {label}
530
- </Text>
531
- {selected ? (
532
- <Ionicons name="checkmark-circle" size={16} color={colors.textPrimary} />
533
- ) : null}
534
- </Pressable>
535
- );
536
- }
537
-
538
580
  function sortChats(chats: ChatSummary[]): ChatSummary[] {
539
581
  return [...chats].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
540
582
  }
@@ -552,121 +594,53 @@ function dedupeChatsById(chats: ChatSummary[]): ChatSummary[] {
552
594
  return Array.from(byId.values());
553
595
  }
554
596
 
555
- function normalizeCwd(value: string | null | undefined): string | null {
556
- if (typeof value !== 'string') {
557
- return null;
558
- }
559
-
560
- const trimmed = value.trim();
561
- return trimmed.length > 0 ? trimmed : null;
562
- }
563
-
564
- function listWorkspaces(chats: ChatSummary[]): string[] {
565
- const sorted = [...chats].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
566
- const seen = new Set<string>();
567
- const result: string[] = [];
568
-
569
- for (const chat of sorted) {
570
- const cwd = normalizeCwd(chat.cwd);
571
- if (!cwd || seen.has(cwd)) {
572
- continue;
573
- }
574
- seen.add(cwd);
575
- result.push(cwd);
597
+ function getDefaultCollapsedWorkspaceKeys(sections: ChatWorkspaceSection[]): Set<string> {
598
+ const collapsed = new Set<string>();
599
+ for (let i = 1; i < sections.length; i += 1) {
600
+ collapsed.add(sections[i].key);
576
601
  }
577
-
578
- return result;
579
- }
580
-
581
- const DEFAULT_WORKSPACE_KEY = '__bridge_default_workspace__';
582
-
583
- function workspaceKey(cwd: string | null): string {
584
- return cwd ?? DEFAULT_WORKSPACE_KEY;
602
+ return collapsed;
585
603
  }
586
604
 
587
- function workspaceTitle(cwd: string | null): string {
588
- if (!cwd) {
589
- return 'Bridge default workspace';
590
- }
591
-
592
- const normalized = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
593
- if (!normalized) {
594
- return cwd;
595
- }
596
-
597
- const lastSlash = normalized.lastIndexOf('/');
598
- if (lastSlash === -1) {
599
- return normalized;
600
- }
605
+ function relativeTime(iso: string): string {
606
+ const diff = Math.max(0, Date.now() - new Date(iso).getTime());
607
+ const minutes = Math.floor(diff / 60000);
608
+ const hours = Math.floor(diff / 3600000);
609
+ const days = Math.floor(diff / 86400000);
610
+ const weeks = Math.floor(days / 7);
601
611
 
602
- return normalized.slice(lastSlash + 1) || normalized;
612
+ if (minutes < 1) return 'now';
613
+ if (minutes < 60) return `${minutes}m`;
614
+ if (hours < 24) return `${hours}h`;
615
+ if (days < 7) return `${days}d`;
616
+ if (weeks < 5) return `${weeks}w`;
617
+ return `${Math.floor(days / 30)}mo`;
603
618
  }
604
619
 
605
- function workspaceSubtitle(cwd: string | null): string | undefined {
606
- if (!cwd) {
607
- return undefined;
620
+ function formatCompactCount(value: number): string {
621
+ if (value >= 1000) {
622
+ return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1).replace(/\.0$/, '')}k`;
608
623
  }
609
624
 
610
- return cwd;
625
+ return String(value);
611
626
  }
612
627
 
613
- function buildWorkspaceSections(chats: ChatSummary[]): ChatWorkspaceSection[] {
614
- if (chats.length === 0) {
615
- return [];
628
+ function formatChatPreview(chat: ChatSummary): string {
629
+ const preview = chat.lastMessagePreview.trim();
630
+ if (preview.length > 0) {
631
+ return preview;
616
632
  }
617
633
 
618
- const sorted = sortChats(chats);
619
- const byWorkspace = new Map<
620
- string,
621
- {
622
- cwd: string | null;
623
- chats: ChatSummary[];
624
- }
625
- >();
626
-
627
- for (const chat of sorted) {
628
- const cwd = normalizeCwd(chat.cwd);
629
- const key = workspaceKey(cwd);
630
- const bucket = byWorkspace.get(key);
631
- if (bucket) {
632
- bucket.chats.push(chat);
633
- continue;
634
- }
635
-
636
- byWorkspace.set(key, {
637
- cwd,
638
- chats: [chat],
639
- });
634
+ const errorPreview = chat.lastError?.trim();
635
+ if (errorPreview) {
636
+ return errorPreview;
640
637
  }
641
638
 
642
- return Array.from(byWorkspace.entries())
643
- .sort(([, a], [, b]) => {
644
- const aUpdatedAt = a.chats[0]?.updatedAt ?? '';
645
- const bUpdatedAt = b.chats[0]?.updatedAt ?? '';
646
- return bUpdatedAt.localeCompare(aUpdatedAt);
647
- })
648
- .map(([key, bucket]) => ({
649
- key,
650
- title: workspaceTitle(bucket.cwd),
651
- subtitle: workspaceSubtitle(bucket.cwd),
652
- data: bucket.chats,
653
- }));
654
- }
655
-
656
- function getDefaultCollapsedWorkspaceKeys(sections: ChatWorkspaceSection[]): Set<string> {
657
- const collapsed = new Set<string>();
658
- for (let i = 1; i < sections.length; i += 1) {
659
- collapsed.add(sections[i].key);
639
+ if (chat.status === 'running') {
640
+ return 'Run in progress';
660
641
  }
661
- return collapsed;
662
- }
663
642
 
664
- function relativeTime(iso: string): string {
665
- const diff = Date.now() - new Date(iso).getTime();
666
- const days = Math.floor(diff / 86400000);
667
- if (days === 0) return 'today';
668
- if (days === 1) return '1d';
669
- return `${days}d`;
643
+ return 'No messages yet';
670
644
  }
671
645
 
672
646
  function toRecord(value: unknown): Record<string, unknown> | null {
@@ -695,7 +669,7 @@ function extractThreadId(event: RpcNotification): string | null {
695
669
  const styles = StyleSheet.create({
696
670
  container: {
697
671
  flex: 1,
698
- backgroundColor: colors.bgSidebar,
672
+ backgroundColor: colors.bgMain,
699
673
  },
700
674
  safeArea: {
701
675
  flex: 1,
@@ -704,175 +678,249 @@ const styles = StyleSheet.create({
704
678
  flex: 1,
705
679
  minHeight: 0,
706
680
  },
707
- brandRow: {
681
+ topDeck: {
682
+ paddingHorizontal: spacing.lg,
683
+ paddingTop: spacing.sm,
684
+ paddingBottom: spacing.md,
685
+ gap: spacing.xs + 2,
686
+ },
687
+ heroCard: {
688
+ borderRadius: 16,
689
+ borderWidth: 1,
690
+ borderColor: 'rgba(255, 255, 255, 0.09)',
691
+ backgroundColor: '#090C10',
692
+ padding: spacing.sm + 2,
693
+ gap: spacing.sm,
694
+ },
695
+ heroHeaderRow: {
708
696
  flexDirection: 'row',
697
+ alignItems: 'flex-start',
698
+ gap: spacing.xs + 2,
699
+ },
700
+ brandBadge: {
701
+ width: 34,
702
+ height: 34,
703
+ borderRadius: 12,
709
704
  alignItems: 'center',
710
- gap: spacing.sm,
711
- paddingHorizontal: spacing.lg,
712
- paddingTop: spacing.md,
713
- paddingBottom: spacing.xs,
705
+ justifyContent: 'center',
706
+ backgroundColor: '#14181D',
707
+ borderWidth: 1,
708
+ borderColor: 'rgba(255, 255, 255, 0.08)',
714
709
  },
715
- brandText: {
710
+ heroCopy: {
711
+ flex: 1,
712
+ gap: 2,
713
+ },
714
+ heroTitle: {
716
715
  ...typography.body,
717
716
  color: colors.textPrimary,
718
- fontWeight: '600',
717
+ fontSize: 16,
718
+ fontWeight: '700',
719
719
  },
720
- header: {
721
- paddingHorizontal: spacing.lg,
722
- paddingTop: spacing.xs,
723
- paddingBottom: spacing.md,
724
- },
725
- workspaceSection: {
726
- paddingHorizontal: spacing.lg,
727
- paddingBottom: spacing.md,
728
- gap: spacing.xs,
720
+ heroSubtitle: {
721
+ ...typography.caption,
722
+ color: colors.textMuted,
723
+ fontSize: 11,
724
+ lineHeight: 14,
729
725
  },
730
- workspacePicker: {
726
+ connectionBadge: {
731
727
  flexDirection: 'row',
732
728
  alignItems: 'center',
733
- gap: spacing.sm,
734
- backgroundColor: colors.bgItem,
735
- borderWidth: StyleSheet.hairlineWidth,
736
- borderColor: colors.borderLight,
737
- borderRadius: 10,
738
- paddingHorizontal: spacing.md,
739
- paddingVertical: spacing.sm,
729
+ gap: 5,
730
+ borderRadius: 999,
731
+ paddingHorizontal: spacing.sm,
732
+ paddingVertical: spacing.xs,
733
+ borderWidth: 1,
734
+ },
735
+ connectionBadgeConnected: {
736
+ backgroundColor: 'rgba(52, 199, 89, 0.12)',
737
+ borderColor: 'rgba(52, 199, 89, 0.32)',
738
+ },
739
+ connectionBadgeDisconnected: {
740
+ backgroundColor: 'rgba(245, 158, 11, 0.12)',
741
+ borderColor: 'rgba(245, 158, 11, 0.28)',
740
742
  },
741
- workspacePickerPressed: {
742
- opacity: 0.85,
743
+ connectionDot: {
744
+ width: 6,
745
+ height: 6,
746
+ borderRadius: 3,
743
747
  },
744
- workspacePickerText: {
748
+ connectionDotConnected: {
749
+ backgroundColor: '#34C759',
750
+ },
751
+ connectionDotDisconnected: {
752
+ backgroundColor: '#F59E0B',
753
+ },
754
+ connectionText: {
745
755
  ...typography.caption,
746
- color: colors.textSecondary,
747
- flex: 1,
756
+ fontSize: 10,
757
+ lineHeight: 12,
758
+ fontWeight: '700',
759
+ textTransform: 'uppercase',
760
+ letterSpacing: 0.5,
761
+ },
762
+ connectionTextConnected: {
763
+ color: '#8EE6AD',
764
+ },
765
+ connectionTextDisconnected: {
766
+ color: '#F6C875',
767
+ },
768
+ heroStatsRow: {
769
+ flexDirection: 'row',
770
+ alignItems: 'stretch',
771
+ borderRadius: 14,
772
+ backgroundColor: '#050608',
773
+ borderWidth: 1,
774
+ borderColor: 'rgba(255, 255, 255, 0.06)',
775
+ overflow: 'hidden',
748
776
  },
749
- newChatBtn: {
750
- marginHorizontal: 0,
751
- marginBottom: 0,
752
- borderWidth: StyleSheet.hairlineWidth,
753
- borderColor: colors.borderLight,
754
- backgroundColor: colors.bgItem,
777
+ heroStat: {
778
+ flex: 1,
779
+ alignItems: 'center',
780
+ gap: 2,
781
+ paddingVertical: spacing.sm,
755
782
  },
756
- newChatText: {
783
+ heroStatValue: {
757
784
  ...typography.body,
758
- fontWeight: '500',
759
785
  color: colors.textPrimary,
786
+ fontSize: 15,
787
+ fontWeight: '700',
788
+ fontVariant: ['tabular-nums'],
760
789
  },
761
- navItem: {
790
+ heroStatLabel: {
791
+ ...typography.caption,
792
+ color: colors.textMuted,
793
+ fontSize: 10,
794
+ lineHeight: 12,
795
+ textTransform: 'uppercase',
796
+ letterSpacing: 0.7,
797
+ },
798
+ heroStatsDivider: {
799
+ width: StyleSheet.hairlineWidth,
800
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
801
+ },
802
+ actionRow: {
803
+ flexDirection: 'row',
804
+ gap: spacing.xs + 2,
805
+ },
806
+ primaryActionButton: {
807
+ flex: 1,
808
+ height: 42,
809
+ borderRadius: 14,
810
+ backgroundColor: '#F2F4F8',
762
811
  flexDirection: 'row',
763
812
  alignItems: 'center',
764
- gap: spacing.md,
765
- paddingHorizontal: spacing.md,
766
- paddingVertical: spacing.md,
767
- marginHorizontal: spacing.md,
768
- borderRadius: 10,
769
- marginBottom: spacing.xs,
813
+ justifyContent: 'center',
814
+ gap: spacing.xs,
770
815
  },
771
- navItemPressed: {
772
- backgroundColor: colors.bgItem,
816
+ primaryActionButtonPressed: {
817
+ opacity: 0.9,
773
818
  },
774
- navLabel: {
819
+ primaryActionText: {
775
820
  ...typography.body,
776
- fontWeight: '500',
777
- color: colors.textPrimary,
821
+ color: colors.black,
822
+ fontWeight: '700',
823
+ fontSize: 14,
778
824
  },
779
825
  sectionHeader: {
826
+ flexDirection: 'row',
827
+ alignItems: 'center',
828
+ justifyContent: 'space-between',
780
829
  paddingHorizontal: spacing.lg,
781
- paddingTop: spacing.md,
782
830
  paddingBottom: spacing.sm,
783
831
  },
784
832
  sectionTitle: {
785
833
  ...typography.caption,
834
+ color: colors.textMuted,
786
835
  textTransform: 'uppercase',
787
- letterSpacing: 0.8,
836
+ fontSize: 10,
837
+ lineHeight: 12,
838
+ letterSpacing: 0.9,
839
+ fontWeight: '700',
840
+ },
841
+ sectionCountBadge: {
842
+ minWidth: 24,
843
+ borderRadius: 999,
844
+ borderWidth: 1,
845
+ borderColor: 'rgba(255, 255, 255, 0.08)',
846
+ backgroundColor: '#101317',
847
+ paddingHorizontal: spacing.sm,
848
+ paddingVertical: 3,
849
+ alignItems: 'center',
850
+ justifyContent: 'center',
851
+ },
852
+ sectionCountText: {
853
+ ...typography.caption,
854
+ color: colors.textSecondary,
855
+ fontSize: 10,
856
+ lineHeight: 12,
857
+ fontWeight: '700',
858
+ fontVariant: ['tabular-nums'],
788
859
  },
789
860
  list: {
790
861
  flex: 1,
791
862
  },
792
863
  listContent: {
793
- paddingBottom: spacing.md,
864
+ paddingBottom: spacing.lg,
794
865
  },
795
866
  loader: {
796
- marginTop: spacing.xl,
797
- },
798
- emptyText: {
799
- ...typography.caption,
800
- textAlign: 'center',
801
- marginTop: spacing.xl,
867
+ marginBottom: spacing.xs,
802
868
  },
803
- chatItem: {
804
- flexDirection: 'row',
869
+ emptyStateCard: {
870
+ marginHorizontal: spacing.lg,
871
+ marginTop: spacing.sm,
872
+ borderRadius: 16,
873
+ borderWidth: 1,
874
+ borderColor: 'rgba(255, 255, 255, 0.08)',
875
+ backgroundColor: '#0B0D10',
876
+ padding: spacing.md,
805
877
  alignItems: 'center',
806
- justifyContent: 'space-between',
807
- paddingHorizontal: spacing.md,
808
- paddingVertical: spacing.md,
809
- marginHorizontal: spacing.md,
810
- borderLeftWidth: StyleSheet.hairlineWidth,
811
- borderRightWidth: StyleSheet.hairlineWidth,
812
- borderBottomWidth: StyleSheet.hairlineWidth,
813
- borderColor: colors.borderLight,
814
- backgroundColor: colors.bgItem,
878
+ gap: spacing.xs + 2,
815
879
  },
816
- chatItemLast: {
817
- borderBottomLeftRadius: 12,
818
- borderBottomRightRadius: 12,
819
- marginBottom: spacing.sm,
820
- },
821
- chatItemSelected: {
822
- backgroundColor: 'rgba(255, 255, 255, 0.08)',
823
- },
824
- chatItemPressed: {
825
- opacity: 0.88,
880
+ emptyStateIconWrap: {
881
+ width: 34,
882
+ height: 34,
883
+ borderRadius: 12,
884
+ alignItems: 'center',
885
+ justifyContent: 'center',
886
+ backgroundColor: '#11151A',
826
887
  },
827
- chatTitle: {
888
+ emptyTitle: {
828
889
  ...typography.body,
829
- color: colors.textMuted,
830
- flex: 1,
831
- marginRight: spacing.sm,
832
- },
833
- chatTitleSelected: {
834
890
  color: colors.textPrimary,
891
+ fontSize: 14,
835
892
  fontWeight: '600',
836
893
  },
837
- chatAge: {
894
+ emptyHint: {
838
895
  ...typography.caption,
839
- flexShrink: 0,
840
- },
841
- chatMeta: {
842
- flexDirection: 'row',
843
- alignItems: 'center',
844
- gap: spacing.xs,
845
- flexShrink: 0,
846
- },
847
- chatSpinner: {
848
- marginRight: 2,
896
+ color: colors.textMuted,
897
+ fontSize: 11,
898
+ textAlign: 'center',
899
+ lineHeight: 15,
849
900
  },
850
901
  workspaceGroupHeader: {
851
- marginHorizontal: spacing.md,
852
- marginTop: spacing.xs,
853
- paddingHorizontal: spacing.md,
854
- paddingVertical: spacing.xs + 3,
855
- borderWidth: StyleSheet.hairlineWidth,
856
- borderColor: colors.borderLight,
857
- backgroundColor: '#15181D',
902
+ marginHorizontal: spacing.lg,
903
+ paddingHorizontal: spacing.sm + 2,
904
+ paddingVertical: spacing.sm,
905
+ borderRadius: 16,
906
+ borderWidth: 1,
907
+ borderColor: 'rgba(255, 255, 255, 0.08)',
908
+ backgroundColor: '#0C0F13',
858
909
  },
859
910
  workspaceGroupHeaderExpanded: {
860
- borderTopLeftRadius: 12,
861
- borderTopRightRadius: 12,
862
- borderBottomLeftRadius: 0,
863
- borderBottomRightRadius: 0,
911
+ marginTop: spacing.xs,
912
+ marginBottom: spacing.xs,
864
913
  },
865
914
  workspaceGroupHeaderCollapsed: {
866
- borderRadius: 12,
867
- marginBottom: spacing.sm,
915
+ marginTop: spacing.xs,
916
+ marginBottom: spacing.md,
868
917
  },
869
918
  workspaceGroupHeaderPressed: {
870
- opacity: 0.8,
919
+ backgroundColor: '#14181D',
871
920
  },
872
921
  workspaceGroupHeaderRow: {
873
922
  flexDirection: 'row',
874
923
  alignItems: 'center',
875
- justifyContent: 'space-between',
876
924
  gap: spacing.sm,
877
925
  },
878
926
  workspaceGroupTitleBlock: {
@@ -881,99 +929,201 @@ const styles = StyleSheet.create({
881
929
  workspaceGroupTitle: {
882
930
  ...typography.body,
883
931
  color: colors.textPrimary,
932
+ fontSize: 14,
884
933
  fontWeight: '600',
885
934
  },
886
935
  workspaceGroupSubtitle: {
887
936
  ...typography.caption,
888
937
  color: colors.textMuted,
889
- marginTop: 1,
938
+ fontSize: 11,
939
+ lineHeight: 14,
940
+ marginTop: 2,
941
+ },
942
+ workspaceGroupCountBadge: {
943
+ minWidth: 24,
944
+ borderRadius: 999,
945
+ backgroundColor: '#161B20',
946
+ paddingHorizontal: spacing.sm,
947
+ paddingVertical: 3,
948
+ alignItems: 'center',
949
+ justifyContent: 'center',
950
+ },
951
+ workspaceGroupCountText: {
952
+ ...typography.caption,
953
+ color: colors.textSecondary,
954
+ fontSize: 10,
955
+ lineHeight: 12,
956
+ fontWeight: '700',
957
+ fontVariant: ['tabular-nums'],
890
958
  },
891
959
  workspaceGroupHeaderMeta: {
892
960
  alignItems: 'center',
893
961
  justifyContent: 'center',
894
962
  flexShrink: 0,
895
963
  },
896
- settingsItem: {
897
- marginBottom: 0,
964
+ chatItem: {
965
+ marginHorizontal: spacing.lg,
966
+ marginBottom: spacing.xs,
967
+ borderRadius: 16,
968
+ borderWidth: 1,
969
+ borderColor: 'rgba(255, 255, 255, 0.06)',
970
+ backgroundColor: '#080A0D',
971
+ padding: spacing.sm,
972
+ flexDirection: 'row',
973
+ gap: spacing.xs + 2,
974
+ alignItems: 'stretch',
898
975
  },
899
- footerNavItem: {
900
- marginBottom: 0,
976
+ chatItemSubAgent: {
977
+ backgroundColor: 'rgba(255, 255, 255, 0.025)',
901
978
  },
902
- footer: {
903
- marginTop: 'auto',
904
- borderTopWidth: StyleSheet.hairlineWidth,
905
- borderTopColor: colors.borderLight,
906
- paddingTop: spacing.md,
907
- paddingBottom: 0,
979
+ chatItemLast: {
980
+ marginBottom: spacing.md,
981
+ },
982
+ chatItemSelected: {
983
+ backgroundColor: '#11151A',
984
+ borderColor: 'rgba(255, 255, 255, 0.18)',
908
985
  },
909
- workspaceModalBackdrop: {
986
+ chatItemPressed: {
987
+ backgroundColor: '#0E1216',
988
+ },
989
+ chatItemAccent: {
990
+ width: 4,
991
+ borderRadius: 999,
992
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
993
+ },
994
+ chatItemAccentSubAgent: {
995
+ backgroundColor: 'rgba(245, 165, 36, 0.35)',
996
+ },
997
+ chatItemAccentSelected: {
998
+ backgroundColor: colors.textPrimary,
999
+ },
1000
+ chatItemAccentRunning: {
1001
+ backgroundColor: colors.statusRunning,
1002
+ },
1003
+ chatItemAccentError: {
1004
+ backgroundColor: colors.statusError,
1005
+ },
1006
+ chatItemContent: {
910
1007
  flex: 1,
911
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
912
- justifyContent: 'center',
913
- paddingHorizontal: spacing.lg,
1008
+ gap: 4,
914
1009
  },
915
- workspaceModalCard: {
916
- backgroundColor: colors.bgSidebar,
917
- borderRadius: 14,
918
- borderWidth: 1,
919
- borderColor: colors.border,
920
- maxHeight: '70%',
921
- padding: spacing.md,
922
- gap: spacing.sm,
1010
+ chatItemTopRow: {
1011
+ flexDirection: 'row',
1012
+ alignItems: 'center',
1013
+ gap: spacing.xs + 2,
923
1014
  },
924
- workspaceModalTitle: {
1015
+ chatSubAgentIcon: {
1016
+ marginRight: -2,
1017
+ },
1018
+ chatTitle: {
925
1019
  ...typography.body,
926
- color: colors.textPrimary,
1020
+ flex: 1,
1021
+ color: colors.textSecondary,
1022
+ fontSize: 14,
927
1023
  fontWeight: '600',
928
1024
  },
929
- workspaceModalList: {
930
- maxHeight: 340,
1025
+ chatTitleSubAgent: {
1026
+ color: '#F5C06A',
1027
+ },
1028
+ chatTitleSelected: {
1029
+ color: colors.textPrimary,
1030
+ },
1031
+ chatAge: {
1032
+ ...typography.caption,
1033
+ color: colors.textMuted,
1034
+ fontSize: 10,
1035
+ lineHeight: 12,
1036
+ fontVariant: ['tabular-nums'],
1037
+ flexShrink: 0,
1038
+ },
1039
+ chatAgeSelected: {
1040
+ color: colors.textSecondary,
1041
+ },
1042
+ chatItemBottomRow: {
1043
+ flexDirection: 'row',
1044
+ alignItems: 'center',
1045
+ gap: spacing.xs + 2,
1046
+ },
1047
+ chatPreview: {
1048
+ ...typography.caption,
1049
+ flex: 1,
1050
+ fontSize: 11,
1051
+ lineHeight: 14,
1052
+ color: 'rgba(232, 236, 244, 0.56)',
1053
+ },
1054
+ chatPreviewSubAgent: {
1055
+ color: 'rgba(245, 192, 106, 0.9)',
1056
+ },
1057
+ chatPreviewSelected: {
1058
+ color: colors.textMuted,
931
1059
  },
932
- workspaceModalListContent: {
1060
+ chatMeta: {
1061
+ flexDirection: 'row',
1062
+ alignItems: 'center',
933
1063
  gap: spacing.xs,
1064
+ flexShrink: 0,
934
1065
  },
935
- workspaceOption: {
1066
+ statusPill: {
936
1067
  flexDirection: 'row',
937
1068
  alignItems: 'center',
938
- justifyContent: 'space-between',
939
- gap: spacing.sm,
940
- borderRadius: 10,
941
- borderWidth: StyleSheet.hairlineWidth,
942
- borderColor: colors.borderLight,
943
- backgroundColor: colors.bgItem,
944
- paddingHorizontal: spacing.md,
945
- paddingVertical: spacing.sm,
1069
+ gap: 4,
1070
+ borderRadius: 999,
1071
+ paddingHorizontal: spacing.xs + 6,
1072
+ paddingVertical: 3,
946
1073
  },
947
- workspaceOptionSelected: {
948
- borderColor: colors.borderHighlight,
949
- backgroundColor: 'rgba(255, 255, 255, 0.08)',
1074
+ statusPillRunning: {
1075
+ backgroundColor: 'rgba(52, 199, 89, 0.12)',
950
1076
  },
951
- workspaceOptionPressed: {
952
- opacity: 0.88,
1077
+ statusPillError: {
1078
+ backgroundColor: 'rgba(239, 68, 68, 0.14)',
953
1079
  },
954
- workspaceOptionText: {
1080
+ statusPillText: {
955
1081
  ...typography.caption,
956
- color: colors.textSecondary,
957
- flex: 1,
1082
+ fontSize: 10,
1083
+ lineHeight: 12,
1084
+ fontWeight: '700',
958
1085
  },
959
- workspaceOptionTextSelected: {
960
- color: colors.textPrimary,
961
- fontWeight: '600',
1086
+ statusPillTextRunning: {
1087
+ color: '#8EE6AD',
962
1088
  },
963
- workspaceModalCloseBtn: {
964
- alignSelf: 'flex-end',
965
- borderRadius: 8,
1089
+ statusPillTextError: {
1090
+ color: '#FFB4B4',
1091
+ },
1092
+ statusPillDot: {
1093
+ width: 6,
1094
+ height: 6,
1095
+ borderRadius: 3,
1096
+ },
1097
+ statusPillDotRunning: {
1098
+ backgroundColor: '#34C759',
1099
+ },
1100
+ footer: {
1101
+ marginTop: 'auto',
1102
+ paddingHorizontal: spacing.lg,
1103
+ paddingTop: spacing.xs,
1104
+ paddingBottom: spacing.sm,
1105
+ },
1106
+ footerSettingsButton: {
1107
+ height: 42,
1108
+ borderRadius: 14,
966
1109
  borderWidth: 1,
967
- borderColor: colors.border,
968
- paddingHorizontal: spacing.md,
969
- paddingVertical: spacing.xs,
970
- marginTop: spacing.xs,
1110
+ borderColor: 'rgba(255, 255, 255, 0.12)',
1111
+ backgroundColor: '#101317',
1112
+ flexDirection: 'row',
1113
+ alignItems: 'center',
1114
+ justifyContent: 'center',
1115
+ gap: spacing.xs,
971
1116
  },
972
- workspaceModalCloseBtnPressed: {
973
- opacity: 0.85,
1117
+ footerSettingsButtonPressed: {
1118
+ backgroundColor: '#171B20',
974
1119
  },
975
- workspaceModalCloseText: {
1120
+ footerSettingsText: {
976
1121
  ...typography.caption,
977
1122
  color: colors.textPrimary,
1123
+ fontSize: 11,
1124
+ lineHeight: 13,
1125
+ fontWeight: '700',
1126
+ textTransform: 'uppercase',
1127
+ letterSpacing: 0.4,
978
1128
  },
979
1129
  });