@base44/superagent-native 0.0.1 → 0.0.2

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -20
  3. package/lib/commonjs/AgentSettingsPanel.js +32 -15
  4. package/lib/commonjs/AgentSettingsPanel.js.map +1 -1
  5. package/lib/commonjs/AttachmentPickerStatusModal.js +2 -2
  6. package/lib/commonjs/AttachmentPickerStatusModal.js.map +1 -1
  7. package/lib/commonjs/ConversationChat.js +27 -11
  8. package/lib/commonjs/ConversationChat.js.map +1 -1
  9. package/lib/commonjs/ConversationComposer.js +10 -6
  10. package/lib/commonjs/ConversationComposer.js.map +1 -1
  11. package/lib/commonjs/ConversationScreen.js +2 -0
  12. package/lib/commonjs/ConversationScreen.js.map +1 -1
  13. package/lib/commonjs/MarkdownText.js +1 -1
  14. package/lib/commonjs/MarkdownText.js.map +1 -1
  15. package/lib/commonjs/MessageActionBar.js +10 -3
  16. package/lib/commonjs/MessageActionBar.js.map +1 -1
  17. package/lib/commonjs/SuperagentHomeScreen.js +17 -3
  18. package/lib/commonjs/SuperagentHomeScreen.js.map +1 -1
  19. package/lib/commonjs/ToolApprovalCard.js +1 -1
  20. package/lib/commonjs/ToolApprovalCard.js.map +1 -1
  21. package/lib/commonjs/ToolCallSummary.js +5 -1
  22. package/lib/commonjs/ToolCallSummary.js.map +1 -1
  23. package/lib/commonjs/attachmentUpload.js +2 -1
  24. package/lib/commonjs/attachmentUpload.js.map +1 -1
  25. package/lib/commonjs/conversationRuntime.js +37 -2
  26. package/lib/commonjs/conversationRuntime.js.map +1 -1
  27. package/lib/commonjs/fileTreeUtils.js +7 -0
  28. package/lib/commonjs/fileTreeUtils.js.map +1 -1
  29. package/lib/commonjs/screenParts.js +3 -3
  30. package/lib/commonjs/styles.js +43 -43
  31. package/lib/commonjs/useSuperagentConversation.js +117 -34
  32. package/lib/commonjs/useSuperagentConversation.js.map +1 -1
  33. package/lib/commonjs/useSuperagentRuntime.js +79 -24
  34. package/lib/commonjs/useSuperagentRuntime.js.map +1 -1
  35. package/lib/module/AgentSettingsPanel.js +32 -15
  36. package/lib/module/AgentSettingsPanel.js.map +1 -1
  37. package/lib/module/AttachmentPickerStatusModal.js +2 -2
  38. package/lib/module/AttachmentPickerStatusModal.js.map +1 -1
  39. package/lib/module/ConversationChat.js +27 -11
  40. package/lib/module/ConversationChat.js.map +1 -1
  41. package/lib/module/ConversationComposer.js +10 -6
  42. package/lib/module/ConversationComposer.js.map +1 -1
  43. package/lib/module/ConversationScreen.js +2 -0
  44. package/lib/module/ConversationScreen.js.map +1 -1
  45. package/lib/module/MarkdownText.js +1 -1
  46. package/lib/module/MarkdownText.js.map +1 -1
  47. package/lib/module/MessageActionBar.js +10 -3
  48. package/lib/module/MessageActionBar.js.map +1 -1
  49. package/lib/module/SuperagentHomeScreen.js +18 -4
  50. package/lib/module/SuperagentHomeScreen.js.map +1 -1
  51. package/lib/module/ToolApprovalCard.js +1 -1
  52. package/lib/module/ToolApprovalCard.js.map +1 -1
  53. package/lib/module/ToolCallSummary.js +5 -1
  54. package/lib/module/ToolCallSummary.js.map +1 -1
  55. package/lib/module/attachmentUpload.js +2 -1
  56. package/lib/module/attachmentUpload.js.map +1 -1
  57. package/lib/module/conversationRuntime.js +36 -2
  58. package/lib/module/conversationRuntime.js.map +1 -1
  59. package/lib/module/fileTreeUtils.js +6 -0
  60. package/lib/module/fileTreeUtils.js.map +1 -1
  61. package/lib/module/screenParts.js +3 -3
  62. package/lib/module/styles.js +43 -43
  63. package/lib/module/useSuperagentConversation.js +118 -35
  64. package/lib/module/useSuperagentConversation.js.map +1 -1
  65. package/lib/module/useSuperagentRuntime.js +80 -25
  66. package/lib/module/useSuperagentRuntime.js.map +1 -1
  67. package/lib/typescript/commonjs/AgentSettingsPanel.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ConversationChat.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ConversationComposer.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ConversationScreen.d.ts +1 -1
  71. package/lib/typescript/commonjs/ConversationScreen.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/SuperagentHomeScreen.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/conversationRuntime.d.ts +3 -2
  74. package/lib/typescript/commonjs/conversationRuntime.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/fileTreeUtils.d.ts +1 -0
  76. package/lib/typescript/commonjs/fileTreeUtils.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/types.d.ts +1 -0
  78. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/useSuperagentConversation.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/useSuperagentRuntime.d.ts +3 -1
  81. package/lib/typescript/commonjs/useSuperagentRuntime.d.ts.map +1 -1
  82. package/lib/typescript/module/AgentSettingsPanel.d.ts.map +1 -1
  83. package/lib/typescript/module/ConversationChat.d.ts.map +1 -1
  84. package/lib/typescript/module/ConversationComposer.d.ts.map +1 -1
  85. package/lib/typescript/module/ConversationScreen.d.ts +1 -1
  86. package/lib/typescript/module/ConversationScreen.d.ts.map +1 -1
  87. package/lib/typescript/module/SuperagentHomeScreen.d.ts.map +1 -1
  88. package/lib/typescript/module/conversationRuntime.d.ts +3 -2
  89. package/lib/typescript/module/conversationRuntime.d.ts.map +1 -1
  90. package/lib/typescript/module/fileTreeUtils.d.ts +1 -0
  91. package/lib/typescript/module/fileTreeUtils.d.ts.map +1 -1
  92. package/lib/typescript/module/types.d.ts +1 -0
  93. package/lib/typescript/module/types.d.ts.map +1 -1
  94. package/lib/typescript/module/useSuperagentConversation.d.ts.map +1 -1
  95. package/lib/typescript/module/useSuperagentRuntime.d.ts +3 -1
  96. package/lib/typescript/module/useSuperagentRuntime.d.ts.map +1 -1
  97. package/package.json +13 -11
  98. package/src/AgentSettingsPanel.tsx +28 -9
  99. package/src/AttachmentPickerStatusModal.tsx +2 -2
  100. package/src/ConversationChat.tsx +37 -9
  101. package/src/ConversationComposer.tsx +11 -6
  102. package/src/ConversationScreen.tsx +2 -0
  103. package/src/MarkdownText.tsx +1 -1
  104. package/src/MessageActionBar.tsx +9 -3
  105. package/src/SuperagentHomeScreen.tsx +18 -3
  106. package/src/ToolApprovalCard.tsx +1 -1
  107. package/src/ToolCallSummary.tsx +4 -1
  108. package/src/attachmentUpload.ts +2 -1
  109. package/src/conversationRuntime.ts +48 -4
  110. package/src/fileTreeUtils.ts +13 -0
  111. package/src/screenParts.tsx +3 -3
  112. package/src/styles.ts +43 -43
  113. package/src/types.ts +1 -0
  114. package/src/useSuperagentConversation.ts +116 -31
  115. package/src/useSuperagentRuntime.ts +80 -24
package/src/styles.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
 
3
3
  export const styles = StyleSheet.create({
4
- safeArea: { flex: 1, backgroundColor: '#000000' },
4
+ safeArea: { flex: 1, backgroundColor: '#F7F7F8' },
5
5
  container: { paddingBottom: 34, paddingHorizontal: 14, paddingTop: 10 },
6
6
  homeTopBar: {
7
7
  alignItems: 'center',
8
- borderBottomColor: '#101012',
8
+ borderBottomColor: '#E4E4E7',
9
9
  borderBottomWidth: 1,
10
10
  flexDirection: 'row',
11
11
  minHeight: 62,
@@ -24,7 +24,7 @@ export const styles = StyleSheet.create({
24
24
  justifyContent: 'center',
25
25
  minWidth: 0,
26
26
  },
27
- topBarTitle: { color: '#F7F7F7', fontSize: 17, fontWeight: '900', letterSpacing: 0, lineHeight: 21 },
27
+ topBarTitle: { color: '#18181B', fontSize: 17, fontWeight: '900', letterSpacing: 0, lineHeight: 21 },
28
28
  homeIntro: { marginBottom: 14, paddingTop: 2 },
29
29
  header: {
30
30
  alignItems: 'center',
@@ -35,13 +35,13 @@ export const styles = StyleSheet.create({
35
35
  paddingTop: 4,
36
36
  },
37
37
  headerAccessory: { flexShrink: 0, marginLeft: 12 },
38
- eyebrow: { color: '#8E8E93', fontSize: 12, fontWeight: '800', marginTop: 2 },
39
- title: { color: '#F7F7F7', fontSize: 24, fontWeight: '900', letterSpacing: 0, lineHeight: 29 },
40
- subtitle: { color: '#A1A1AA', fontSize: 14, fontWeight: '600', lineHeight: 20, marginTop: 5 },
38
+ eyebrow: { color: '#71717A', fontSize: 12, fontWeight: '800', marginTop: 2 },
39
+ title: { color: '#18181B', fontSize: 24, fontWeight: '900', letterSpacing: 0, lineHeight: 29 },
40
+ subtitle: { color: '#52525B', fontSize: 14, fontWeight: '600', lineHeight: 20, marginTop: 5 },
41
41
  headerAvatar: {
42
42
  alignItems: 'center',
43
- backgroundColor: '#151515',
44
- borderColor: '#2A2A2A',
43
+ backgroundColor: '#FFFFFF',
44
+ borderColor: '#E4E4E7',
45
45
  borderRadius: 22,
46
46
  borderWidth: 1,
47
47
  height: 44,
@@ -50,15 +50,15 @@ export const styles = StyleSheet.create({
50
50
  },
51
51
  loadingPanel: {
52
52
  alignItems: 'center',
53
- backgroundColor: '#0B0B0C',
54
- borderColor: '#242427',
53
+ backgroundColor: '#FFFFFF',
54
+ borderColor: '#E4E4E7',
55
55
  borderRadius: 18,
56
56
  borderWidth: 1,
57
57
  padding: 32,
58
58
  },
59
59
  heroPanel: {
60
- backgroundColor: '#0B0B0C',
61
- borderColor: '#242427',
60
+ backgroundColor: '#FFFFFF',
61
+ borderColor: '#E4E4E7',
62
62
  borderRadius: 22,
63
63
  borderWidth: 1,
64
64
  overflow: 'hidden',
@@ -70,7 +70,7 @@ export const styles = StyleSheet.create({
70
70
  justifyContent: 'space-between',
71
71
  marginBottom: 16,
72
72
  },
73
- heroLabel: { color: '#8E8E93', fontSize: 11, fontWeight: '900', textTransform: 'uppercase' },
73
+ heroLabel: { color: '#71717A', fontSize: 11, fontWeight: '900', textTransform: 'uppercase' },
74
74
  agentCard: {
75
75
  alignItems: 'flex-start',
76
76
  flexDirection: 'row',
@@ -78,23 +78,23 @@ export const styles = StyleSheet.create({
78
78
  },
79
79
  avatar: {
80
80
  alignItems: 'center',
81
- backgroundColor: '#111111',
82
- borderColor: '#2A2A2A',
81
+ backgroundColor: '#F4F4F5',
82
+ borderColor: '#E4E4E7',
83
83
  borderRadius: 24,
84
84
  borderWidth: 1,
85
85
  height: 52,
86
86
  justifyContent: 'center',
87
87
  width: 52,
88
88
  },
89
- avatarText: { color: '#F7F7F7', fontSize: 18, fontWeight: '900' },
89
+ avatarText: { color: '#18181B', fontSize: 18, fontWeight: '900' },
90
90
  agentCardBody: { flex: 1, marginLeft: 13, minWidth: 0 },
91
- agentName: { color: '#F7F7F7', fontSize: 21, fontWeight: '900', lineHeight: 25 },
92
- agentDescription: { color: '#BDBDC2', fontSize: 14, fontWeight: '600', lineHeight: 20, marginTop: 7 },
93
- chevron: { color: '#8E8E93', fontSize: 20, fontWeight: '900' },
91
+ agentName: { color: '#18181B', fontSize: 21, fontWeight: '900', lineHeight: 25 },
92
+ agentDescription: { color: '#52525B', fontSize: 14, fontWeight: '600', lineHeight: 20, marginTop: 7 },
93
+ chevron: { color: '#A1A1AA', fontSize: 20, fontWeight: '900' },
94
94
  agentMetaRow: { flexDirection: 'row', flexWrap: 'wrap', marginLeft: 65, marginTop: 14 },
95
95
  agentMetaPill: {
96
- backgroundColor: '#151515',
97
- borderColor: '#2A2A2A',
96
+ backgroundColor: '#F4F4F5',
97
+ borderColor: '#E4E4E7',
98
98
  borderRadius: 13,
99
99
  borderWidth: 1,
100
100
  marginRight: 7,
@@ -102,20 +102,20 @@ export const styles = StyleSheet.create({
102
102
  paddingHorizontal: 10,
103
103
  paddingVertical: 6,
104
104
  },
105
- agentMetaText: { color: '#D4D4D8', fontSize: 12, fontWeight: '800' },
105
+ agentMetaText: { color: '#3F3F46', fontSize: 12, fontWeight: '800' },
106
106
  emptyPanel: {
107
- backgroundColor: '#0B0B0C',
108
- borderColor: '#242427',
107
+ backgroundColor: '#FFFFFF',
108
+ borderColor: '#E4E4E7',
109
109
  borderRadius: 22,
110
110
  borderWidth: 1,
111
111
  padding: 16,
112
112
  },
113
- emptyTitle: { color: '#F7F7F7', fontSize: 23, fontWeight: '900', lineHeight: 28, marginTop: 15 },
114
- emptyBody: { color: '#BDBDC2', fontSize: 14, fontWeight: '600', lineHeight: 21, marginTop: 8 },
113
+ emptyTitle: { color: '#18181B', fontSize: 23, fontWeight: '900', lineHeight: 28, marginTop: 15 },
114
+ emptyBody: { color: '#52525B', fontSize: 14, fontWeight: '600', lineHeight: 21, marginTop: 8 },
115
115
  emptyChipRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 14 },
116
116
  emptyChip: {
117
- backgroundColor: '#151515',
118
- borderColor: '#2A2A2A',
117
+ backgroundColor: '#F4F4F5',
118
+ borderColor: '#E4E4E7',
119
119
  borderRadius: 15,
120
120
  borderWidth: 1,
121
121
  marginRight: 7,
@@ -123,21 +123,21 @@ export const styles = StyleSheet.create({
123
123
  paddingHorizontal: 11,
124
124
  paddingVertical: 7,
125
125
  },
126
- emptyChipText: { color: '#D4D4D8', fontSize: 12, fontWeight: '800' },
126
+ emptyChipText: { color: '#3F3F46', fontSize: 12, fontWeight: '800' },
127
127
  primaryButton: {
128
128
  alignItems: 'center',
129
- backgroundColor: '#F4F4F5',
129
+ backgroundColor: '#18181B',
130
130
  borderRadius: 17,
131
131
  justifyContent: 'center',
132
132
  marginTop: 20,
133
133
  minHeight: 50,
134
134
  paddingHorizontal: 18,
135
135
  },
136
- primaryButtonText: { color: '#111111', fontSize: 16, fontWeight: '900' },
136
+ primaryButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '900' },
137
137
  secondaryButton: {
138
138
  alignItems: 'center',
139
- backgroundColor: '#151515',
140
- borderColor: '#2A2A2A',
139
+ backgroundColor: '#FFFFFF',
140
+ borderColor: '#E4E4E7',
141
141
  borderRadius: 16,
142
142
  borderWidth: 1,
143
143
  flexDirection: 'row',
@@ -146,7 +146,7 @@ export const styles = StyleSheet.create({
146
146
  minHeight: 46,
147
147
  paddingHorizontal: 16,
148
148
  },
149
- secondaryButtonText: { color: '#F4F4F5', fontSize: 14, fontWeight: '900', marginLeft: 7 },
149
+ secondaryButtonText: { color: '#18181B', fontSize: 14, fontWeight: '900', marginLeft: 7 },
150
150
  recentSection: { marginTop: 22 },
151
151
  sectionHeader: {
152
152
  alignItems: 'center',
@@ -154,12 +154,12 @@ export const styles = StyleSheet.create({
154
154
  justifyContent: 'space-between',
155
155
  marginBottom: 10,
156
156
  },
157
- sectionTitle: { color: '#F4F4F5', fontSize: 17, fontWeight: '900' },
158
- sectionCount: { color: '#73737A', fontSize: 12, fontWeight: '900' },
157
+ sectionTitle: { color: '#18181B', fontSize: 17, fontWeight: '900' },
158
+ sectionCount: { color: '#A1A1AA', fontSize: 12, fontWeight: '900' },
159
159
  agentRow: {
160
160
  alignItems: 'center',
161
- backgroundColor: '#0B0B0C',
162
- borderColor: '#242427',
161
+ backgroundColor: '#FFFFFF',
162
+ borderColor: '#E4E4E7',
163
163
  borderRadius: 17,
164
164
  borderWidth: 1,
165
165
  flexDirection: 'row',
@@ -170,12 +170,12 @@ export const styles = StyleSheet.create({
170
170
  paddingVertical: 10,
171
171
  },
172
172
  agentRowBody: { flex: 1, marginLeft: 11, minWidth: 0 },
173
- agentRowName: { color: '#F4F4F5', fontSize: 15, fontWeight: '900' },
174
- agentRowDescription: { color: '#8E8E93', fontSize: 12, fontWeight: '600', lineHeight: 17, marginTop: 3 },
173
+ agentRowName: { color: '#18181B', fontSize: 15, fontWeight: '900' },
174
+ agentRowDescription: { color: '#71717A', fontSize: 12, fontWeight: '600', lineHeight: 17, marginTop: 3 },
175
175
  compactAvatar: {
176
176
  alignItems: 'center',
177
- backgroundColor: '#111111',
178
- borderColor: '#2A2A2A',
177
+ backgroundColor: '#F4F4F5',
178
+ borderColor: '#E4E4E7',
179
179
  borderRadius: 15,
180
180
  borderWidth: 1,
181
181
  height: 38,
@@ -185,6 +185,6 @@ export const styles = StyleSheet.create({
185
185
  placeholder: { flex: 1, padding: 20 },
186
186
  backButton: { alignSelf: 'flex-start', marginBottom: 24, paddingVertical: 8 },
187
187
  backButtonText: { color: '#FF5A1F', fontSize: 16, fontWeight: '700' },
188
- helperText: { color: '#73737A', fontSize: 12, fontWeight: '700', lineHeight: 18, marginTop: 12 },
188
+ helperText: { color: '#A1A1AA', fontSize: 12, fontWeight: '700', lineHeight: 18, marginTop: 12 },
189
189
  pressed: { opacity: 0.72 },
190
190
  });
package/src/types.ts CHANGED
@@ -421,6 +421,7 @@ export type SuperagentMarkdownRendererProps = {
421
421
  export type SuperagentMarkdownRenderer = ComponentType<SuperagentMarkdownRendererProps>;
422
422
 
423
423
  export type SuperagentHomeScreenProps = {
424
+ activeAgentId?: string | null;
424
425
  agents?: SuperagentAgent[];
425
426
  latestMessages?: SuperagentMessage[];
426
427
  messagesByAgentId?: Record<string, SuperagentMessage[]>;
@@ -9,6 +9,7 @@ import {
9
9
  hasNewAssistantResponse,
10
10
  isQueuedResponse,
11
11
  isVisibleMessage,
12
+ mergeConversationSnapshot,
12
13
  mergeMessage,
13
14
  normalizeMessages,
14
15
  pollConversation,
@@ -48,6 +49,35 @@ export function useSuperagentConversation({
48
49
  const [initError, setInitError] = useState<string | null>(null);
49
50
  const [hasMoreMessages, setHasMoreMessages] = useState(false);
50
51
  const sendGenerationRef = useRef(0);
52
+ const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
53
+ const loadingPreviousRef = useRef(false);
54
+ const settleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
55
+ const messagesRef = useRef(messages);
56
+ const onAgentMessageDoneRef = useRef(onAgentMessageDone);
57
+ const onConversationSettledRef = useRef(onConversationSettled);
58
+
59
+ useEffect(() => { messagesRef.current = messages; }, [messages]);
60
+ useEffect(() => {
61
+ onAgentMessageDoneRef.current = onAgentMessageDone;
62
+ onConversationSettledRef.current = onConversationSettled;
63
+ }, [onAgentMessageDone, onConversationSettled]);
64
+
65
+ // Cancel any in-flight poll/settle timers — called on each new send and on unmount.
66
+ const clearPendingSettlers = useCallback(() => {
67
+ if (pollIntervalRef.current) {
68
+ clearInterval(pollIntervalRef.current);
69
+ pollIntervalRef.current = null;
70
+ }
71
+ if (settleTimeoutRef.current) {
72
+ clearTimeout(settleTimeoutRef.current);
73
+ settleTimeoutRef.current = null;
74
+ }
75
+ }, []);
76
+
77
+ // Clear any active poll interval / settle timeout when the hook unmounts.
78
+ useEffect(() => {
79
+ return () => clearPendingSettlers();
80
+ }, [clearPendingSettlers]);
51
81
 
52
82
  useEffect(() => {
53
83
  if (!apiClient) setMessages(fallbackMessages);
@@ -94,34 +124,45 @@ export function useSuperagentConversation({
94
124
 
95
125
  return realtimeClient.subscribeToConversation(conversationId, {
96
126
  onMessage(message) {
127
+ // Don't clear the sending/stop state on intermediate assistant messages
128
+ // (tool-call steps or streamed content arrive before the turn is done).
129
+ // The turn ends on `agent_done` (onAgentDone) or the 5s settle fallback.
97
130
  setMessages((current) => mergeMessage(current, message));
98
- if (message.role === 'assistant' && (message.content || message.toolCalls?.length || message.tool_calls?.length)) {
99
- setIsSending(false);
100
- }
101
131
  },
102
132
  onConversation(conversation) {
103
133
  if (conversation.id !== conversationId) return;
104
- setMessages(normalizeMessages(conversation.messages ?? []));
134
+ const incoming = normalizeMessages(conversation.messages ?? []);
135
+ setMessages((current) => mergeConversationSnapshot(current, incoming));
105
136
  },
106
137
  onAgentDone() {
138
+ // Agent finished via realtime — cancel the 5s settle fallback so
139
+ // onConversationSettled doesn't also fire when the timer elapses.
140
+ if (settleTimeoutRef.current) {
141
+ clearTimeout(settleTimeoutRef.current);
142
+ settleTimeoutRef.current = null;
143
+ }
107
144
  setIsSending(false);
108
- onAgentMessageDone?.();
145
+ setQueuedMessages([]);
146
+ onAgentMessageDoneRef.current?.();
109
147
  if (apiClient) {
110
148
  refreshConversation(apiClient, conversationId, setMessages)
111
- .then(() => onConversationSettled?.())
149
+ .then(() => onConversationSettledRef.current?.())
112
150
  .catch(() => {});
113
151
  } else {
114
- onConversationSettled?.();
152
+ onConversationSettledRef.current?.();
115
153
  }
116
154
  },
117
155
  onReconnect() {
156
+ // A reconnect means the transient socket error cleared — drop the
157
+ // "something went wrong" state so it doesn't linger as a failed load.
158
+ setInitError(null);
118
159
  if (apiClient) refreshConversation(apiClient, conversationId, setMessages).catch(() => {});
119
160
  },
120
161
  onError(error) {
121
162
  setInitError(error instanceof Error ? error.message : 'Realtime connection failed');
122
163
  },
123
164
  });
124
- }, [apiClient, conversationId, onAgentMessageDone, onConversationSettled, realtimeClient]);
165
+ }, [apiClient, conversationId, realtimeClient]);
125
166
 
126
167
  const visibleMessages = useMemo(
127
168
  () => messages.filter((message, index) => isVisibleMessage(message, index)),
@@ -133,12 +174,20 @@ export function useSuperagentConversation({
133
174
  const fileUrls = options.fileUrls?.filter(Boolean) ?? [];
134
175
  if (!trimmedContent && fileUrls.length === 0) return;
135
176
 
136
- const assistantCountBefore = countAssistantResponses(messages);
177
+ // A new send supersedes any pending poll/settle timers from the previous one.
178
+ clearPendingSettlers();
179
+
180
+ const assistantCountBefore = countAssistantResponses(messagesRef.current);
137
181
  const userMessage = createUserMessage(trimmedContent, { fileUrls, replyTo: options.replyTo });
138
182
  setMessages((current) => [...current, userMessage]);
139
183
 
140
184
  if (!apiClient || !conversationId) {
141
- if (!onSendMessage) return;
185
+ if (!onSendMessage) {
186
+ // No send path yet (no fallback handler, or the conversation is still
187
+ // loading) — drop the optimistic bubble so the turn doesn't look sent.
188
+ setMessages((current) => current.filter((message) => message.id !== userMessage.id));
189
+ return;
190
+ }
142
191
  const sendGeneration = sendGenerationRef.current + 1;
143
192
  sendGenerationRef.current = sendGeneration;
144
193
  const setSendingForCurrentSend = (value: boolean) => {
@@ -147,7 +196,7 @@ export function useSuperagentConversation({
147
196
 
148
197
  setIsSending(true);
149
198
  const hasAssistantResponse = await sendWithFallback(agentId, trimmedContent, fileUrls, options.replyTo, onSendMessage, setMessages, setSendingForCurrentSend);
150
- if (hasAssistantResponse) onAgentMessageDone?.();
199
+ if (hasAssistantResponse) onAgentMessageDoneRef.current?.();
151
200
  return;
152
201
  }
153
202
 
@@ -159,24 +208,41 @@ export function useSuperagentConversation({
159
208
 
160
209
  setIsSending(true);
161
210
  try {
211
+ // Wait for the turn to complete: poll without realtime, otherwise a 5s
212
+ // fallback in case agent_done never arrives.
213
+ const armSettleFallback = () => {
214
+ if (!realtimeClient) {
215
+ pollIntervalRef.current = pollConversation(
216
+ apiClient, conversationId, setMessages, setSendingForCurrentSend, assistantCountBefore,
217
+ () => { setQueuedMessages([]); onConversationSettledRef.current?.(); },
218
+ () => onAgentMessageDoneRef.current?.(),
219
+ );
220
+ } else {
221
+ settleTimeoutRef.current = setTimeout(() => {
222
+ settleTimeoutRef.current = null;
223
+ refreshConversation(apiClient, conversationId, setMessages).catch(() => {});
224
+ setSendingForCurrentSend(false);
225
+ setQueuedMessages([]);
226
+ onAgentMessageDoneRef.current?.();
227
+ onConversationSettledRef.current?.();
228
+ }, 5000);
229
+ }
230
+ };
231
+
162
232
  const response = await apiClient.addMessage(conversationId, userMessage);
163
233
  if (isQueuedResponse(response)) {
164
234
  setQueuedMessages((current) => [...current, { id: userMessage.id!, content: userMessage.content, position: current.length }]);
235
+ // A queued send still needs a settle path, or isSending / the queued strip stick.
236
+ armSettleFallback();
165
237
  } else {
166
238
  const refreshedMessages = await refreshConversation(apiClient, conversationId, setMessages);
167
- if (!realtimeClient) {
168
- if (hasNewAssistantResponse(refreshedMessages, assistantCountBefore)) {
169
- setSendingForCurrentSend(false);
170
- onAgentMessageDone?.();
171
- onConversationSettled?.();
172
- } else {
173
- pollConversation(apiClient, conversationId, setMessages, setSendingForCurrentSend, assistantCountBefore, onConversationSettled, onAgentMessageDone);
174
- }
239
+ if (!realtimeClient && hasNewAssistantResponse(refreshedMessages, assistantCountBefore)) {
240
+ setSendingForCurrentSend(false);
241
+ setQueuedMessages([]);
242
+ onAgentMessageDoneRef.current?.();
243
+ onConversationSettledRef.current?.();
175
244
  } else {
176
- setTimeout(() => {
177
- setSendingForCurrentSend(false);
178
- onConversationSettled?.();
179
- }, 5000);
245
+ armSettleFallback();
180
246
  }
181
247
  }
182
248
  } catch (error) {
@@ -184,7 +250,7 @@ export function useSuperagentConversation({
184
250
  setMessages((current) => [...current, errorMessage]);
185
251
  setSendingForCurrentSend(false);
186
252
  }
187
- }, [agentId, apiClient, conversationId, messages, onAgentMessageDone, onConversationSettled, onSendMessage, realtimeClient]);
253
+ }, [agentId, apiClient, clearPendingSettlers, conversationId, onSendMessage, realtimeClient]);
188
254
 
189
255
  const deleteMessage = useCallback(async (messageId: string) => {
190
256
  if (!messageId || messageId === 'welcome') return false;
@@ -196,16 +262,23 @@ export function useSuperagentConversation({
196
262
 
197
263
  try {
198
264
  await apiClient.deleteMessage(conversationId, messageId);
265
+ // Remove locally before refreshing: mergeConversationSnapshot keeps rows
266
+ // missing from the server snapshot, so without this the just-deleted
267
+ // message would be resurrected.
268
+ setMessages((current) => current.filter((message) => message.id !== messageId));
199
269
  await refreshConversation(apiClient, conversationId, setMessages);
200
270
  return true;
201
- } catch (error) {
202
- setInitError(error instanceof Error ? error.message : 'Failed to delete message');
271
+ } catch {
272
+ // A failed delete is not a load failure — don't drive the "something went
273
+ // wrong" panel (it would persist). Signal failure via the return value so
274
+ // the caller can surface it.
203
275
  return false;
204
276
  }
205
277
  }, [apiClient, conversationId]);
206
278
 
207
279
  const stop = useCallback(async () => {
208
280
  sendGenerationRef.current += 1;
281
+ clearPendingSettlers();
209
282
 
210
283
  if (!apiClient || !conversationId) {
211
284
  setIsSending(false);
@@ -216,20 +289,32 @@ export function useSuperagentConversation({
216
289
  setQueuedMessages([]);
217
290
  await apiClient.stopConversation(conversationId);
218
291
  await refreshConversation(apiClient, conversationId, setMessages);
219
- }, [apiClient, conversationId]);
292
+ }, [apiClient, clearPendingSettlers, conversationId]);
220
293
 
221
294
  const loadPrevious = useCallback(async () => {
295
+ if (loadingPreviousRef.current) return;
222
296
  const before = getMessageCursor(messages[0]);
223
297
  if (!apiClient || !conversationId || !before) return;
224
- const page = await apiClient.getMessages(conversationId, { limit: MESSAGES_PAGE_SIZE, before });
225
- setMessages((current) => [...normalizeMessages(page.messages), ...current]);
226
- setHasMoreMessages(Boolean(page.has_more ?? page.hasMore));
298
+ loadingPreviousRef.current = true;
299
+ try {
300
+ const page = await apiClient.getMessages(conversationId, { limit: MESSAGES_PAGE_SIZE, before });
301
+ setMessages((current) => [...normalizeMessages(page.messages), ...current]);
302
+ setHasMoreMessages(Boolean(page.has_more ?? page.hasMore));
303
+ } catch {
304
+ // Pagination failed — swallow so it isn't an unhandled rejection from the
305
+ // button, and leave hasMoreMessages as-is so the user can retry.
306
+ } finally {
307
+ loadingPreviousRef.current = false;
308
+ }
227
309
  }, [apiClient, conversationId, messages]);
228
310
 
229
311
  const submitToolCallInput = useCallback(async (toolCallId: string, approve: boolean, extraUserInput?: unknown, originRequestId?: string): Promise<SuperagentConversation | null> => {
230
312
  if (!apiClient || !conversationId) return null;
231
313
  const updated = await apiClient.submitToolCallInput(conversationId, toolCallId, approve, extraUserInput, originRequestId);
232
- if (updated.messages) setMessages(normalizeMessages(updated.messages));
314
+ if (updated.messages) {
315
+ const incoming = normalizeMessages(updated.messages);
316
+ setMessages((current) => mergeConversationSnapshot(current, incoming));
317
+ }
233
318
  onConversationSettled?.();
234
319
  return updated;
235
320
  }, [apiClient, conversationId, onConversationSettled]);