@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
@@ -121,9 +121,13 @@ export function AgentSettingsPanel({
121
121
  const [newSecretValue, setNewSecretValue] = useState('');
122
122
  const [isModelPickerOpen, setIsModelPickerOpen] = useState(false);
123
123
  const [pendingAction, setPendingAction] = useState<PendingAction>(null);
124
+ const [deletingSecretName, setDeletingSecretName] = useState<string | null>(null);
124
125
  const [visibleSecrets, setVisibleSecrets] = useState<Set<string>>(new Set());
125
126
 
126
- const permissions = normalizePermissions(agent.toolsPermissionConfig);
127
+ const permissions = useMemo(
128
+ () => normalizePermissions(agent.toolsPermissionConfig),
129
+ [agent.toolsPermissionConfig],
130
+ );
127
131
  const selectedModel = useMemo(
128
132
  () => MODEL_OPTIONS.find((model) => model.id === (agent.model || 'default')) ?? MODEL_OPTIONS[0],
129
133
  [agent.model],
@@ -140,8 +144,12 @@ export function AgentSettingsPanel({
140
144
  );
141
145
 
142
146
  useEffect(() => {
147
+ // Seed guard drafts only when switching agents — not on every
148
+ // toolsPermissionConfig change — so in-progress guard edits aren't wiped by
149
+ // an unrelated permission toggle before "Save rules" is pressed.
143
150
  setGuardDrafts(permissions.connector_guards);
144
- }, [agent.id, permissions.connector_guards]);
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [agent.id]);
145
153
 
146
154
  const runAction = async (action: Exclude<PendingAction, null>, handler: () => Promise<void> | void) => {
147
155
  if (pendingAction) {
@@ -211,11 +219,17 @@ export function AgentSettingsPanel({
211
219
  return;
212
220
  }
213
221
 
214
- await runAction('save-secret', async () => {
215
- await onSaveSecret({ agentId: agent.id, name, value });
216
- setNewSecretName('');
217
- setNewSecretValue('');
218
- });
222
+ // Only clear the inputs after a successful save; if onSaveSecret rejects it
223
+ // already surfaced the error, and we keep the typed values so they aren't lost.
224
+ try {
225
+ await runAction('save-secret', async () => {
226
+ await onSaveSecret({ agentId: agent.id, name, value });
227
+ setNewSecretName('');
228
+ setNewSecretValue('');
229
+ });
230
+ } catch {
231
+ // onSaveSecret already surfaced the error; keep the form populated.
232
+ }
219
233
  };
220
234
 
221
235
  const deleteSecret = async (secret: SuperagentSecret) => {
@@ -223,7 +237,12 @@ export function AgentSettingsPanel({
223
237
  return;
224
238
  }
225
239
 
226
- await runAction('delete-secret', () => onDeleteSecret({ agentId: agent.id, name: secret.name }));
240
+ setDeletingSecretName(secret.name);
241
+ try {
242
+ await runAction('delete-secret', () => onDeleteSecret({ agentId: agent.id, name: secret.name }));
243
+ } finally {
244
+ setDeletingSecretName(null);
245
+ }
227
246
  };
228
247
 
229
248
  return (
@@ -308,7 +327,7 @@ export function AgentSettingsPanel({
308
327
  onPress={() => deleteSecret(secret)}
309
328
  style={({ pressed }) => [localStyles.iconButtonDanger, pressed && sharedStyles.pressed]}
310
329
  >
311
- {pendingAction === 'delete-secret' ? (
330
+ {deletingSecretName === secret.name ? (
312
331
  <ActivityIndicator color="#FCA5A5" size="small" />
313
332
  ) : (
314
333
  <Trash2 color="#FCA5A5" size={16} strokeWidth={2.4} />
@@ -19,9 +19,9 @@ export function AttachmentPickerStatusModal({
19
19
  {isError ? null : <ActivityIndicator color="#F4F4F5" style={styles.loader} />}
20
20
  <Text style={styles.title}>{title}</Text>
21
21
  <Text style={[styles.message, isError ? styles.error : undefined]}>{message}</Text>
22
- {isError ? (
22
+ {onClose ? (
23
23
  <Pressable accessibilityRole="button" onPress={onClose} style={styles.closeButton}>
24
- <Text style={styles.closeText}>Close</Text>
24
+ <Text style={styles.closeText}>{isError ? 'Close' : 'Cancel'}</Text>
25
25
  </Pressable>
26
26
  ) : null}
27
27
  </View>
@@ -63,10 +63,23 @@ export function ConversationChat({
63
63
  const [draft, setDraft] = useState('');
64
64
  const [attachments, setAttachments] = useState<SuperagentMediaAttachment[]>([]);
65
65
  const [replyTo, setReplyTo] = useState<SuperagentReplyTo | null>(null);
66
- const canSend = (draft.trim().length > 0 || attachments.length > 0) && !conversation.isSending;
66
+ // Conversation failed to initialize (apiClient path): no conversation id to send
67
+ // against, so block sends rather than clear the composer and drop the text.
68
+ const conversationUnavailable = Boolean(conversation.initError && !conversation.conversationId);
69
+ const canSend = (draft.trim().length > 0 || attachments.length > 0)
70
+ && !conversation.isSending
71
+ && !conversation.isLoading
72
+ && !conversationUnavailable;
67
73
  const displayedMessages = useMemo(
68
- () => (conversation.messages.length > 0 ? conversation.messages : [createWelcomeMessage(agent)]),
69
- [agent, conversation.messages],
74
+ () => {
75
+ if (conversation.messages.length > 0) return conversation.messages;
76
+ // While the real conversation is still loading, don't fabricate a welcome
77
+ // bubble — it would render next to the loading panel as if the agent already
78
+ // sent an intro.
79
+ if (conversation.isLoading) return [];
80
+ return [createWelcomeMessage(agent)];
81
+ },
82
+ [agent, conversation.messages, conversation.isLoading],
70
83
  );
71
84
  const tailMessage = displayedMessages[displayedMessages.length - 1];
72
85
  const tailMessageId = tailMessage ? getMessageAutoScrollId(tailMessage, displayedMessages.length - 1) : null;
@@ -104,7 +117,10 @@ export function ConversationChat({
104
117
  const selectedAttachments = attachments;
105
118
  const selectedReplyTo = replyTo;
106
119
  const content = draft.trim() || buildAttachmentPrompt(selectedAttachments);
107
- if ((!content && selectedAttachments.length === 0) || conversation.isSending) return;
120
+ // Don't send (and clear the composer) until the conversation is ready — while
121
+ // it's still loading or failed to init the hook can't reach the server and
122
+ // would drop the text.
123
+ if ((!content && selectedAttachments.length === 0) || conversation.isSending || conversation.isLoading || conversationUnavailable) return;
108
124
 
109
125
  setDraft('');
110
126
  setAttachments([]);
@@ -114,7 +130,7 @@ export function ConversationChat({
114
130
  fileUrls: selectedAttachments.map((attachment) => attachment.url),
115
131
  replyTo: selectedReplyTo ?? undefined,
116
132
  });
117
- }, [attachments, conversation, draft, replyTo]);
133
+ }, [attachments, conversation, conversationUnavailable, draft, replyTo]);
118
134
 
119
135
  const addAttachments = useCallback((incoming: SuperagentMediaAttachment[]) => {
120
136
  setAttachments((current) => [...current, ...incoming].slice(0, MAX_ATTACHMENTS));
@@ -125,7 +141,7 @@ export function ConversationChat({
125
141
  }, []);
126
142
 
127
143
  const replyToMessage = useCallback((message: SuperagentMessage) => {
128
- const content = message.content.trim();
144
+ const content = message.content?.trim();
129
145
  if (!content) return;
130
146
  setReplyTo({
131
147
  content,
@@ -133,6 +149,18 @@ export function ConversationChat({
133
149
  });
134
150
  }, []);
135
151
 
152
+ const composerContext = useMemo(
153
+ () => ({ agentId: agent.id, conversationId: conversation.conversationId }),
154
+ [agent.id, conversation.conversationId],
155
+ );
156
+
157
+ const clearReply = useCallback(() => setReplyTo(null), []);
158
+
159
+ const removeAttachment = useCallback(
160
+ (index: number) => setAttachments((current) => current.filter((_, itemIndex) => itemIndex !== index)),
161
+ [],
162
+ );
163
+
136
164
  return (
137
165
  <>
138
166
  <ScrollView
@@ -167,17 +195,17 @@ export function ConversationChat({
167
195
  <ConversationComposer
168
196
  attachments={attachments}
169
197
  canSend={canSend}
170
- context={{ agentId: agent.id, conversationId: conversation.conversationId }}
198
+ context={composerContext}
171
199
  draft={draft}
172
200
  isSending={conversation.isSending}
173
201
  onAddAttachments={addAttachments}
174
202
  onAppendTranscript={appendTranscript}
175
203
  onChangeDraft={setDraft}
176
- onClearReply={() => setReplyTo(null)}
204
+ onClearReply={clearReply}
177
205
  onImportFromDrive={onImportFromDrive}
178
206
  onPickFiles={onPickFiles}
179
207
  onPickPhotos={onPickPhotos}
180
- onRemoveAttachment={(index) => setAttachments((current) => current.filter((_, itemIndex) => itemIndex !== index))}
208
+ onRemoveAttachment={removeAttachment}
181
209
  onSend={sendMessage}
182
210
  onStartLiveVoice={onStartLiveVoice}
183
211
  onStartVoiceInput={onStartVoiceInput}
@@ -185,7 +185,8 @@ export function ConversationComposer({
185
185
  isLiveVoiceActive={liveVoiceState === 'processing'}
186
186
  isVoiceInputActive={voiceState === 'processing'}
187
187
  onSend={onSend}
188
- onStartLiveVoice={onStartLiveVoice ? startLiveVoice : onStartVoiceInput ? startVoiceInput : undefined}
188
+ onStartVoice={context.conversationId ? (onStartLiveVoice ? startLiveVoice : onStartVoiceInput ? startVoiceInput : undefined) : undefined}
189
+ voiceMode={context.conversationId ? (onStartLiveVoice ? 'live' : onStartVoiceInput ? 'input' : null) : null}
189
190
  onStop={onStop}
190
191
  isSending={isSending}
191
192
  />
@@ -321,7 +322,8 @@ function ComposerActionButton({
321
322
  isSending,
322
323
  isVoiceInputActive,
323
324
  onSend,
324
- onStartLiveVoice,
325
+ onStartVoice,
326
+ voiceMode,
325
327
  onStop,
326
328
  }: {
327
329
  canSend: boolean;
@@ -330,7 +332,8 @@ function ComposerActionButton({
330
332
  isSending: boolean;
331
333
  isVoiceInputActive: boolean;
332
334
  onSend: () => void;
333
- onStartLiveVoice?: () => void;
335
+ onStartVoice?: () => void;
336
+ voiceMode: 'live' | 'input' | null;
334
337
  onStop: () => void;
335
338
  }) {
336
339
  if (isSending) {
@@ -342,12 +345,14 @@ function ComposerActionButton({
342
345
  }
343
346
 
344
347
  if (isDraftEmpty) {
345
- const disabled = !onStartLiveVoice;
348
+ const disabled = !onStartVoice;
349
+ const isActive = voiceMode === 'live' ? isLiveVoiceActive : isVoiceInputActive;
350
+ const label = voiceMode === 'input' ? 'Start voice input' : 'Start Live Voice';
346
351
  return (
347
- <Pressable accessibilityLabel="Start Live Voice" accessibilityRole="button" disabled={disabled} onPress={onStartLiveVoice} style={({ pressed }) => [
352
+ <Pressable accessibilityLabel={label} accessibilityRole="button" disabled={disabled} onPress={onStartVoice} style={({ pressed }) => [
348
353
  composerStyles.sendButton,
349
354
  composerStyles.liveActionButton,
350
- (isLiveVoiceActive || isVoiceInputActive) && composerStyles.activeButton,
355
+ isActive && composerStyles.activeButton,
351
356
  disabled && composerStyles.sendButtonDisabled,
352
357
  pressed && styles.pressed,
353
358
  ]}>
@@ -31,6 +31,7 @@ export function ConversationScreen({
31
31
  filePaths,
32
32
  isLoadingAgentSettings,
33
33
  isLoadingAutomations,
34
+ isLoadingChannels,
34
35
  isLoadingCollaborators,
35
36
  isLoadingConnectors,
36
37
  isLoadingFiles,
@@ -246,6 +247,7 @@ export function ConversationScreen({
246
247
  fileLoadFailed={fileLoadFailed}
247
248
  filePaths={filePaths}
248
249
  isLoadingAgentSettings={isLoadingAgentSettings}
250
+ isLoadingChannels={isLoadingChannels}
249
251
  isLoadingCollaborators={isLoadingCollaborators}
250
252
  isLoadingFiles={isLoadingFiles}
251
253
  isLoadingAutomations={isLoadingAutomations}
@@ -237,7 +237,7 @@ function isTableRow(line?: string) {
237
237
  function isTableSeparator(line?: string) {
238
238
  if (!line) return false;
239
239
  const cells = splitTableRow(line);
240
- return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
240
+ return cells.length >= 2 && cells.every((cell) => /^:?-+:?$/.test(cell.replace(/\s+/g, '')));
241
241
  }
242
242
 
243
243
  function splitTableRow(line: string) {
@@ -95,7 +95,7 @@ function getMessageActions({
95
95
  }): MessageAction[] {
96
96
  const messageId = message.id;
97
97
  const isPreviewMessage = messageId === 'welcome';
98
- const hasContent = message.content.trim().length > 0;
98
+ const hasContent = (message.content?.trim().length ?? 0) > 0;
99
99
  const actions: MessageAction[] = [];
100
100
 
101
101
  if (!isPreviewMessage && hasContent && onReplyMessage) {
@@ -112,12 +112,18 @@ function getMessageActions({
112
112
  });
113
113
  }
114
114
 
115
- if (!isPreviewMessage && messageId && onDeleteMessage) {
115
+ // Only content-bearing messages are deletable. Tool-only bubbles can be a
116
+ // merge of several tool turns under a synthetic `a:b` id the server never
117
+ // issued, so deleting by that id would fail.
118
+ if (!isPreviewMessage && hasContent && messageId && onDeleteMessage) {
116
119
  actions.push({
117
120
  destructive: true,
118
121
  label: 'Delete',
119
122
  run: async () => {
120
- await onDeleteMessage(messageId);
123
+ const deleted = await onDeleteMessage(messageId);
124
+ if (deleted === false) {
125
+ Alert.alert('Delete failed', 'The message could not be deleted. Please try again.');
126
+ }
121
127
  },
122
128
  });
123
129
  }
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  SafeAreaView,
@@ -19,6 +19,7 @@ export function SuperagentHomeScreen(props: SuperagentHomeScreenProps) {
19
19
  }
20
20
 
21
21
  function SuperagentHomeScreenContent({
22
+ activeAgentId,
22
23
  agents = [],
23
24
  automations,
24
25
  automationCredits,
@@ -106,9 +107,22 @@ function SuperagentHomeScreenContent({
106
107
  }: SuperagentHomeScreenProps) {
107
108
  const [route, setRoute] = useState<SuperagentRoute>(initialRoute);
108
109
  const [isCreating, setIsCreating] = useState(false);
110
+
111
+ // Follow externally-driven route changes (e.g. the runtime moves home after the
112
+ // active agent is deleted, or a deep link changes the agent). Keyed by value so
113
+ // it doesn't clobber internal navigation on unrelated re-renders.
114
+ const externalRouteKey = initialRoute.name === 'agent' ? `agent:${initialRoute.agentId}` : initialRoute.name;
115
+ useEffect(() => {
116
+ setRoute(initialRoute);
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [externalRouteKey]);
109
119
  const latestAgent = agents[0] ?? null;
110
- const automationCount = automations?.length ?? 0;
111
- const connectedConnectorCount = connectedConnectors?.length ?? 0;
120
+ // automations/connectedConnectors track only the active agent, so showing their
121
+ // counts on the card is wrong unless the card IS the active agent. Otherwise fall
122
+ // back to 0 (AgentCard renders the neutral "Tools ready" / "No tasks yet").
123
+ const statsMatchLatest = latestAgent != null && latestAgent.id === activeAgentId;
124
+ const automationCount = statsMatchLatest ? (automations?.length ?? 0) : 0;
125
+ const connectedConnectorCount = statsMatchLatest ? (connectedConnectors?.length ?? 0) : 0;
112
126
 
113
127
  const navigate = useCallback((nextRoute: SuperagentRoute) => {
114
128
  setRoute(nextRoute);
@@ -151,6 +165,7 @@ function SuperagentHomeScreenContent({
151
165
 
152
166
  return (
153
167
  <ConversationScreen
168
+ key={route.agentId}
154
169
  apiClient={apiClient}
155
170
  agent={activeAgent ?? createUnknownAgent(route.agentId)}
156
171
  automationCredits={automationCredits}
@@ -383,7 +383,7 @@ function ApprovalInput({
383
383
  function getApprovalKind(toolCall: SuperagentToolCall, guardData: Record<string, unknown> | null): ApprovalKind {
384
384
  if (guardData) return 'guard';
385
385
  if (toolCall.name === 'setup_slack_connection' || toolCall.name === 'setup_telegram_connection') return 'channel';
386
- if (toolCall.name === 'set_app_user_connector') return 'customConnector';
386
+ if (toolCall.name === 'set_app_user_connector' || toolCall.name === 'register_workspace_connector') return 'customConnector';
387
387
  if (toolCall.name === 'request_oauth_authorization') return 'connector';
388
388
  if (toolCall.name === 'install_npm_package') return 'package';
389
389
  if (toolCall.name === 'suggest_payments_installation' || toolCall.name === 'suggest_stripe_installation') return 'payment';
@@ -169,7 +169,10 @@ function DefaultToolTimeline({
169
169
 
170
170
  const label = getCollapsedLabel(visibleToolCalls, isRunning);
171
171
 
172
- if (isRunning) {
172
+ // Only collapse to the bare running indicator when nothing needs the user's
173
+ // input. If an approval/waiting tool is present alongside a running one, fall
174
+ // through to the expanded timeline so the approval card stays reachable.
175
+ if (isRunning && !hasWaitingTool) {
173
176
  return (
174
177
  <View style={conversationStyles.toolCallSlot}>
175
178
  <RunningToolIndicator label={label} />
@@ -92,7 +92,8 @@ async function uploadNativeItem({
92
92
  },
93
93
  method: 'POST',
94
94
  });
95
- const body = await response.json().catch(() => ({}));
95
+ const parsed = await response.json().catch(() => null);
96
+ const body = parsed && typeof parsed === 'object' ? parsed : {};
96
97
 
97
98
  if (!response.ok || typeof body.url !== 'string') {
98
99
  throw new Error(body.message || body.detail || 'Upload failed.');
@@ -22,7 +22,15 @@ export function mergeMessage(messages: SuperagentMessage[], nextMessage: Superag
22
22
  const existingIndex = messages.findIndex((message) => message.id === normalized.id);
23
23
  if (existingIndex < 0) return [...messages, normalized];
24
24
 
25
- return messages.map((message, index) => (index === existingIndex ? normalized : message));
25
+ return messages.map((message, index) => {
26
+ if (index !== existingIndex) return message;
27
+ // Don't let a shorter/stale assistant payload overwrite newer streamed text
28
+ // the user is already reading (mirrors the onConversation snapshot merge).
29
+ if (message.role === 'assistant' && (message.content?.length ?? 0) > (normalized.content?.length ?? 0)) {
30
+ return message;
31
+ }
32
+ return normalized;
33
+ });
26
34
  }
27
35
 
28
36
  export function isVisibleMessage(message: SuperagentMessage, index: number) {
@@ -78,14 +86,46 @@ export function hasNewAssistantResponse(messages: SuperagentMessage[], assistant
78
86
  return countAssistantResponses(messages) > assistantCountBefore;
79
87
  }
80
88
 
89
+ // Merge an authoritative server snapshot with the current list, keeping a local
90
+ // assistant row when it already has more text than the snapshot copy — so a
91
+ // slightly stale fetch/event can't shorten streamed content the user is reading.
92
+ export function mergeConversationSnapshot(
93
+ current: SuperagentMessage[],
94
+ incoming: SuperagentMessage[],
95
+ ): SuperagentMessage[] {
96
+ const currentById = new Map(current.filter((message) => message.id).map((message) => [message.id, message]));
97
+ const incomingIds = new Set(incoming.map((message) => message.id).filter(Boolean));
98
+ const incomingUserContents = new Set(
99
+ incoming.filter((message) => message.role === 'user').map((message) => (message.content ?? '').trim()),
100
+ );
101
+ const merged = incoming.map((message) => {
102
+ const local = message.id ? currentById.get(message.id) : undefined;
103
+ if (local && local.role === 'assistant' && (local.content?.length ?? 0) > (message.content?.length ?? 0)) {
104
+ return local;
105
+ }
106
+ return message;
107
+ });
108
+ // Keep local rows the snapshot hasn't caught up to (optimistic sends / messages
109
+ // still streaming in), dropping an optimistic user echo the snapshot already
110
+ // represents by content so it isn't duplicated. Callers that genuinely remove a
111
+ // message (delete) must drop it from local state before refreshing, or it would
112
+ // be resurrected here.
113
+ const pendingLocal = current.filter((message) =>
114
+ message.id
115
+ && !incomingIds.has(message.id)
116
+ && !(message.role === 'user' && incomingUserContents.has((message.content ?? '').trim())),
117
+ );
118
+ return [...merged, ...pendingLocal];
119
+ }
120
+
81
121
  export async function refreshConversation(
82
122
  apiClient: SuperagentNativeClient,
83
123
  conversationId: string,
84
- setMessages: (messages: SuperagentMessage[]) => void,
124
+ setMessages: Dispatch<SetStateAction<SuperagentMessage[]>>,
85
125
  ) {
86
126
  const refreshed = await apiClient.getConversation(conversationId);
87
127
  const normalizedMessages = normalizeMessages(refreshed.messages ?? []);
88
- setMessages(normalizedMessages);
128
+ setMessages((current) => mergeConversationSnapshot(current, normalizedMessages));
89
129
  return normalizedMessages;
90
130
  }
91
131
 
@@ -116,7 +156,7 @@ export async function sendWithFallback(
116
156
  export function pollConversation(
117
157
  apiClient: SuperagentNativeClient,
118
158
  conversationId: string,
119
- setMessages: (messages: SuperagentMessage[]) => void,
159
+ setMessages: Dispatch<SetStateAction<SuperagentMessage[]>>,
120
160
  setIsSending: (value: boolean) => void,
121
161
  assistantCountBefore: number,
122
162
  onComplete?: () => Promise<void> | void,
@@ -132,6 +172,8 @@ export function pollConversation(
132
172
  const refreshedMessages = await refreshConversation(apiClient, conversationId, setMessages);
133
173
  foundAssistantResponse = hasNewAssistantResponse(refreshedMessages, assistantCountBefore);
134
174
  shouldStop = shouldStop || foundAssistantResponse;
175
+ } catch {
176
+ // Swallow transient poll failures; keep polling until the attempt cap.
135
177
  } finally {
136
178
  if (shouldStop) {
137
179
  clearInterval(interval);
@@ -141,6 +183,8 @@ export function pollConversation(
141
183
  }
142
184
  }
143
185
  }, 2000);
186
+
187
+ return interval;
144
188
  }
145
189
 
146
190
  export function getMessageCursor(message?: SuperagentMessage) {
@@ -15,6 +15,19 @@ export const DEFAULT_SANDBOX_FILE_PATHS = [
15
15
 
16
16
  export type SuperagentFileCategory = 'code' | 'html' | 'image' | 'markdown' | 'pdf' | 'text';
17
17
 
18
+ export function sanitizeSandboxFilePath(path: string) {
19
+ // Strip absolute / `..` / `.` segments and null bytes so a picked file name can't
20
+ // write outside the sandbox via path traversal (defense in depth, not a substitute
21
+ // for the backend's own validation).
22
+ return path
23
+ .replace(/\\/g, '/')
24
+ .replace(/\0/g, '')
25
+ .split('/')
26
+ .map((segment) => segment.trim())
27
+ .filter((segment) => segment && segment !== '.' && segment !== '..')
28
+ .join('/');
29
+ }
30
+
18
31
  export function normalizeFilePaths(paths: string[] = []) {
19
32
  return paths
20
33
  .filter((path) => typeof path === 'string' && path.trim().length > 0)
@@ -44,7 +44,7 @@ export function AgentCard({
44
44
  {preview || agent.description || 'Open your latest Superagent conversation.'}
45
45
  </Text>
46
46
  </View>
47
- <ChevronRight color="#8E8E93" size={24} strokeWidth={2.5} />
47
+ <ChevronRight color="#A1A1AA" size={24} strokeWidth={2.5} />
48
48
  </View>
49
49
  <View style={styles.agentMetaRow}>
50
50
  {meta.map((item) => (
@@ -118,7 +118,7 @@ export function AgentRow({
118
118
  {preview || agent.description || 'Ready for the next instruction.'}
119
119
  </Text>
120
120
  </View>
121
- <ChevronRight color="#73737A" size={21} strokeWidth={2.5} />
121
+ <ChevronRight color="#A1A1AA" size={21} strokeWidth={2.5} />
122
122
  </Pressable>
123
123
  );
124
124
  }
@@ -145,7 +145,7 @@ export function CreateAgentButton({
145
145
  onPress={onPress}
146
146
  style={({ pressed }) => [styles.secondaryButton, pressed && styles.pressed]}
147
147
  >
148
- <Plus color="#F4F4F5" size={18} strokeWidth={2.6} />
148
+ <Plus color="#18181B" size={18} strokeWidth={2.6} />
149
149
  <Text style={styles.secondaryButtonText}>{isCreating ? 'Creating...' : 'New Superagent'}</Text>
150
150
  </Pressable>
151
151
  );