@agent-native/core 0.95.0 → 0.95.1

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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +10 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +13 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +18 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +16 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +17 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +16 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +17 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +15 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +16 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +11 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +12 -0
  13. package/corpus/core/docs/content/template-design.mdx +16 -0
  14. package/corpus/core/package.json +2 -2
  15. package/corpus/core/src/agent/production-agent.ts +20 -0
  16. package/corpus/core/src/agent/thread-data-builder.ts +41 -17
  17. package/corpus/core/src/client/AssistantChat.tsx +250 -25
  18. package/corpus/core/src/client/chat/tool-call-display.tsx +1 -1
  19. package/corpus/core/src/client/chat/use-reconnect-reader-owner.ts +28 -0
  20. package/corpus/core/src/client/guided-questions.tsx +9 -1
  21. package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +6 -7
  22. package/corpus/core/src/client/sse-event-processor.ts +90 -21
  23. package/corpus/core/src/connections/catalog.ts +2 -2
  24. package/corpus/core/src/deploy/build.ts +3 -0
  25. package/corpus/core/src/integrations/catalog.ts +2 -2
  26. package/corpus/core/src/server/auth.ts +1 -1
  27. package/corpus/core/src/server/credential-provider.ts +7 -3
  28. package/corpus/core/src/shared/cache-control.ts +10 -4
  29. package/corpus/templates/analytics/app/lib/data-sources.ts +5 -4
  30. package/corpus/templates/analytics/changelog/2026-07-10-slack-token-setup-marked-legacy.md +6 -0
  31. package/corpus/templates/analytics/server/lib/credential-keys.ts +6 -2
  32. package/corpus/templates/brain/changelog/2026-07-10-slack-backfill-token-marked-legacy.md +6 -0
  33. package/corpus/templates/brain/server/register-secrets.ts +2 -2
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -1
  35. package/corpus/templates/clips/app/components/library/page-header.tsx +19 -3
  36. package/corpus/templates/clips/app/routes/_app.library._index.tsx +16 -12
  37. package/corpus/templates/clips/changelog/2026-07-10-new-recording-appears-once-when-the-library-sidebar-is-open.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-10-slack-token-fallback-guidance.md +6 -0
  39. package/corpus/templates/clips/server/register-secrets.ts +1 -1
  40. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +0 -66
  41. package/corpus/templates/content/app/routes/_app.local-files.tsx +7 -47
  42. package/corpus/templates/content/changelog/2026-07-10-local-folders-now-wait-for-an-explicit-pull-before-content-r.md +6 -0
  43. package/corpus/templates/content/package.json +1 -1
  44. package/corpus/templates/design/actions/generate-design.ts +128 -31
  45. package/corpus/templates/design/actions/present-design-variants.ts +37 -9
  46. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +9 -3
  47. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +32 -10
  48. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +2 -2
  49. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +9 -1
  50. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -5
  51. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  52. package/corpus/templates/design/app/global.css +10 -0
  53. package/corpus/templates/design/app/hooks/use-question-flow.ts +9 -2
  54. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  55. package/corpus/templates/design/app/i18n-data.ts +21 -11
  56. package/corpus/templates/design/changelog/2026-07-10-editor-panels-are-easier-to-distinguish-and-design-intake-qu.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-10-interact-mode-now-stays-on-the-full-canvas-matches-edit-visu.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-10-new-web-designs-open-at-desktop-size-and-adapt-across-mobile.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-10-tweaks-now-link-to-full-documentation.md +6 -0
  60. package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
  61. package/corpus/templates/mail/app/i18n/ar-SA.ts +3 -3
  62. package/corpus/templates/mail/app/i18n/de-DE.ts +3 -3
  63. package/corpus/templates/mail/app/i18n/en-US.ts +4 -3
  64. package/corpus/templates/mail/app/i18n/es-ES.ts +3 -3
  65. package/corpus/templates/mail/app/i18n/fr-FR.ts +3 -3
  66. package/corpus/templates/mail/app/i18n/hi-IN.ts +3 -3
  67. package/corpus/templates/mail/app/i18n/ja-JP.ts +3 -3
  68. package/corpus/templates/mail/app/i18n/ko-KR.ts +3 -3
  69. package/corpus/templates/mail/app/i18n/pt-BR.ts +3 -3
  70. package/corpus/templates/mail/app/i18n/zh-CN.ts +3 -3
  71. package/corpus/templates/mail/app/i18n/zh-TW.ts +3 -3
  72. package/corpus/templates/mail/changelog/2026-07-10-slack-intake-marked-legacy.md +6 -0
  73. package/corpus/templates/mail/server/lib/env-config.ts +5 -4
  74. package/corpus/templates/plan/package.json +1 -1
  75. package/dist/agent/production-agent.d.ts.map +1 -1
  76. package/dist/agent/production-agent.js +20 -0
  77. package/dist/agent/production-agent.js.map +1 -1
  78. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  79. package/dist/agent/thread-data-builder.js +36 -18
  80. package/dist/agent/thread-data-builder.js.map +1 -1
  81. package/dist/client/AssistantChat.d.ts +3 -1
  82. package/dist/client/AssistantChat.d.ts.map +1 -1
  83. package/dist/client/AssistantChat.js +200 -28
  84. package/dist/client/AssistantChat.js.map +1 -1
  85. package/dist/client/chat/tool-call-display.js +1 -1
  86. package/dist/client/chat/tool-call-display.js.map +1 -1
  87. package/dist/client/chat/use-reconnect-reader-owner.d.ts +11 -0
  88. package/dist/client/chat/use-reconnect-reader-owner.d.ts.map +1 -0
  89. package/dist/client/chat/use-reconnect-reader-owner.js +21 -0
  90. package/dist/client/chat/use-reconnect-reader-owner.js.map +1 -0
  91. package/dist/client/guided-questions.d.ts.map +1 -1
  92. package/dist/client/guided-questions.js +5 -1
  93. package/dist/client/guided-questions.js.map +1 -1
  94. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  95. package/dist/client/integrations/IntegrationsPanel.js +5 -7
  96. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  97. package/dist/client/sse-event-processor.d.ts.map +1 -1
  98. package/dist/client/sse-event-processor.js +67 -20
  99. package/dist/client/sse-event-processor.js.map +1 -1
  100. package/dist/collab/routes.d.ts +1 -1
  101. package/dist/connections/catalog.d.ts +2 -2
  102. package/dist/connections/catalog.js +2 -2
  103. package/dist/connections/catalog.js.map +1 -1
  104. package/dist/deploy/build.d.ts.map +1 -1
  105. package/dist/deploy/build.js +3 -1
  106. package/dist/deploy/build.js.map +1 -1
  107. package/dist/integrations/catalog.js +2 -2
  108. package/dist/integrations/catalog.js.map +1 -1
  109. package/dist/notifications/routes.d.ts +2 -2
  110. package/dist/observability/routes.d.ts +1 -1
  111. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  112. package/dist/resources/handlers.d.ts +1 -1
  113. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  114. package/dist/server/auth.js +1 -1
  115. package/dist/server/auth.js.map +1 -1
  116. package/dist/server/credential-provider.d.ts +5 -3
  117. package/dist/server/credential-provider.d.ts.map +1 -1
  118. package/dist/server/credential-provider.js +7 -3
  119. package/dist/server/credential-provider.js.map +1 -1
  120. package/dist/shared/cache-control.d.ts +8 -6
  121. package/dist/shared/cache-control.d.ts.map +1 -1
  122. package/dist/shared/cache-control.js +8 -3
  123. package/dist/shared/cache-control.js.map +1 -1
  124. package/docs/content/locales/ar-SA/template-design.mdx +13 -0
  125. package/docs/content/locales/de-DE/template-design.mdx +18 -0
  126. package/docs/content/locales/es-ES/template-design.mdx +16 -0
  127. package/docs/content/locales/fr-FR/template-design.mdx +17 -0
  128. package/docs/content/locales/hi-IN/template-design.mdx +16 -0
  129. package/docs/content/locales/ja-JP/template-design.mdx +17 -0
  130. package/docs/content/locales/ko-KR/template-design.mdx +15 -0
  131. package/docs/content/locales/pt-BR/template-design.mdx +16 -0
  132. package/docs/content/locales/zh-CN/template-design.mdx +11 -0
  133. package/docs/content/locales/zh-TW/template-design.mdx +12 -0
  134. package/docs/content/template-design.mdx +16 -0
  135. package/package.json +2 -2
@@ -123,6 +123,7 @@ import {
123
123
  type ApprovalContextValue,
124
124
  ReconnectStreamMessage,
125
125
  } from "./chat/tool-call-display.js";
126
+ import { useReconnectReaderOwner } from "./chat/use-reconnect-reader-owner.js";
126
127
  import {
127
128
  MessageScroller,
128
129
  MessageScrollerButton,
@@ -764,6 +765,20 @@ function toolCallIdsRepresentSameLocalCall(
764
765
  return renderedId === reconnectId || renderedId.endsWith(`:${reconnectId}`);
765
766
  }
766
767
 
768
+ function toolCallIdIsReaderLocal(id: string): boolean {
769
+ return (
770
+ /^tc_\d+$/.test(id) ||
771
+ /:tc_\d+$/.test(id) ||
772
+ id.startsWith("reconnect-activity:")
773
+ );
774
+ }
775
+
776
+ function toolCallReaderLocalKey(id: string): string | null {
777
+ const counterMatch = id.match(/(?:^|:)(tc_\d+)$/);
778
+ if (counterMatch?.[1]) return counterMatch[1];
779
+ return id.startsWith("reconnect-activity:") ? id : null;
780
+ }
781
+
767
782
  function collectRenderedToolCallStates(messages: readonly unknown[]): {
768
783
  byId: Map<string, { rank: number }>;
769
784
  latestAssistantByFingerprint: Map<string, { rank: number }>;
@@ -862,7 +877,14 @@ function latestRenderedAssistantText(messages: readonly unknown[]): string {
862
877
  function dedupePendingToolCallReplaysWithinContent(
863
878
  content: ContentPart[],
864
879
  ): ContentPart[] {
865
- const seenLaterFingerprints = new Set<string>();
880
+ const laterStatesByFingerprint = new Map<
881
+ string,
882
+ {
883
+ ids: Set<string>;
884
+ readerLocalKeys: Set<string>;
885
+ hasStableId: boolean;
886
+ }
887
+ >();
866
888
  let changed = false;
867
889
  const nextReversed: ContentPart[] = [];
868
890
 
@@ -870,53 +892,153 @@ function dedupePendingToolCallReplaysWithinContent(
870
892
  const part = content[i];
871
893
  if (!part) continue;
872
894
  const fingerprint = toolCallFingerprintFromContentPart(part);
873
- if (
874
- fingerprint &&
875
- !toolCallPartHasResult(part) &&
876
- seenLaterFingerprints.has(fingerprint)
877
- ) {
895
+ const laterState = fingerprint
896
+ ? laterStatesByFingerprint.get(fingerprint)
897
+ : undefined;
898
+ const id = toolCallIdFromContentPart(part);
899
+ const readerLocalKey = id ? toolCallReaderLocalKey(id) : null;
900
+ const isReaderReplay = Boolean(
901
+ id &&
902
+ laterState &&
903
+ (laterState.ids.has(id) ||
904
+ (readerLocalKey && laterState.readerLocalKeys.has(readerLocalKey)) ||
905
+ (toolCallIdIsReaderLocal(id) && laterState.hasStableId)),
906
+ );
907
+ if (fingerprint && !toolCallPartHasResult(part) && isReaderReplay) {
878
908
  changed = true;
879
909
  continue;
880
910
  }
881
- if (fingerprint) seenLaterFingerprints.add(fingerprint);
911
+ if (fingerprint) {
912
+ const state = laterState ?? {
913
+ ids: new Set(),
914
+ readerLocalKeys: new Set(),
915
+ hasStableId: false,
916
+ };
917
+ if (id) {
918
+ state.ids.add(id);
919
+ const key = toolCallReaderLocalKey(id);
920
+ if (key) state.readerLocalKeys.add(key);
921
+ else state.hasStableId = true;
922
+ }
923
+ laterStatesByFingerprint.set(fingerprint, state);
924
+ }
882
925
  nextReversed.push(part);
883
926
  }
884
927
 
885
928
  return changed ? nextReversed.reverse() : content;
886
929
  }
887
930
 
931
+ function pendingToolCallCountsByName(
932
+ content: readonly ContentPart[],
933
+ ): Map<string, number> {
934
+ const counts = new Map<string, number>();
935
+ for (const part of content) {
936
+ const name = toolCallNameFromContentPart(part);
937
+ if (!name || toolCallProgressRank(part) >= 4) continue;
938
+ counts.set(name, (counts.get(name) ?? 0) + 1);
939
+ }
940
+ return counts;
941
+ }
942
+
943
+ /**
944
+ * Linear-time longest overlap where a suffix of `renderedText` equals a prefix
945
+ * of `reconnectText`. The reconnect overlay is recalculated during render on
946
+ * every stream tick, so a descending slice/endsWith scan becomes quadratic on
947
+ * large repeated output.
948
+ */
949
+ function longestSuffixPrefixOverlap(
950
+ renderedText: string,
951
+ reconnectText: string,
952
+ ): number {
953
+ const maxOverlap = Math.min(renderedText.length, reconnectText.length);
954
+ if (maxOverlap === 0) return 0;
955
+
956
+ const pattern = reconnectText.slice(0, maxOverlap);
957
+ const prefixLengths = new Array<number>(pattern.length).fill(0);
958
+ for (let index = 1; index < pattern.length; index += 1) {
959
+ let matched = prefixLengths[index - 1] ?? 0;
960
+ while (matched > 0 && pattern[index] !== pattern[matched]) {
961
+ matched = prefixLengths[matched - 1] ?? 0;
962
+ }
963
+ if (pattern[index] === pattern[matched]) matched += 1;
964
+ prefixLengths[index] = matched;
965
+ }
966
+
967
+ let matched = 0;
968
+ const renderedStart = renderedText.length - maxOverlap;
969
+ for (let index = renderedStart; index < renderedText.length; index += 1) {
970
+ const character = renderedText[index];
971
+ while (matched > 0 && character !== pattern[matched]) {
972
+ matched = prefixLengths[matched - 1] ?? 0;
973
+ }
974
+ if (character === pattern[matched]) matched += 1;
975
+ if (matched === pattern.length && index < renderedText.length - 1) {
976
+ matched = prefixLengths[matched - 1] ?? 0;
977
+ }
978
+ }
979
+ return matched;
980
+ }
981
+
888
982
  function trimReconnectTextAlreadyRendered(
889
983
  content: ContentPart[],
890
984
  renderedAssistantText: string,
985
+ options?: { trimTailOverlap?: boolean },
891
986
  ): ContentPart[] {
892
987
  if (!renderedAssistantText) return content;
893
988
 
894
- let remainingOverlap = renderedAssistantText;
989
+ const reconnectText = assistantTextFromContent(content);
990
+ if (!reconnectText) return content;
991
+
992
+ let overlapLength = 0;
993
+ if (reconnectText.startsWith(renderedAssistantText)) {
994
+ overlapLength = renderedAssistantText.length;
995
+ } else if (renderedAssistantText.startsWith(reconnectText)) {
996
+ overlapLength = reconnectText.length;
997
+ } else if (options?.trimTailOverlap) {
998
+ const tailOverlap = longestSuffixPrefixOverlap(
999
+ renderedAssistantText,
1000
+ reconnectText,
1001
+ );
1002
+ if (tailOverlap > 0) {
1003
+ // Tail reconnects begin after the last remembered event, so their first
1004
+ // text can be the final suffix of a thread snapshot imported in parallel.
1005
+ // For a partial overlap, require a whole word/phrase boundary so a
1006
+ // coincidental shared character at the join cannot eat legitimate text.
1007
+ const renderedBeforeOverlap =
1008
+ renderedAssistantText[renderedAssistantText.length - tailOverlap - 1];
1009
+ const reconnectAfterOverlap = reconnectText[tailOverlap];
1010
+ const isWordCharacter = (value: string | undefined) =>
1011
+ value ? /[\p{L}\p{N}_]/u.test(value) : false;
1012
+ const isWholeBoundary =
1013
+ !isWordCharacter(renderedBeforeOverlap) &&
1014
+ !isWordCharacter(reconnectAfterOverlap);
1015
+ if (tailOverlap === reconnectText.length || isWholeBoundary) {
1016
+ overlapLength = tailOverlap;
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ if (overlapLength === 0) return content;
1022
+
1023
+ let remainingOverlap = overlapLength;
895
1024
  let changed = false;
896
1025
  const next: ContentPart[] = [];
897
1026
 
898
1027
  for (const part of content) {
899
- if (part.type !== "text" || remainingOverlap.length === 0) {
1028
+ if (part.type !== "text" || remainingOverlap === 0) {
900
1029
  next.push(part);
901
1030
  continue;
902
1031
  }
903
1032
 
904
- if (remainingOverlap.startsWith(part.text)) {
905
- remainingOverlap = remainingOverlap.slice(part.text.length);
906
- changed = true;
907
- continue;
908
- }
909
-
910
- if (part.text.startsWith(remainingOverlap)) {
911
- const text = part.text.slice(remainingOverlap.length);
912
- remainingOverlap = "";
1033
+ if (part.text.length <= remainingOverlap) {
1034
+ remainingOverlap -= part.text.length;
913
1035
  changed = true;
914
- if (text) next.push({ ...part, text });
915
1036
  continue;
916
1037
  }
917
1038
 
918
- remainingOverlap = "";
919
- next.push(part);
1039
+ next.push({ ...part, text: part.text.slice(remainingOverlap) });
1040
+ remainingOverlap = 0;
1041
+ changed = true;
920
1042
  }
921
1043
 
922
1044
  return changed ? next : content;
@@ -925,10 +1047,14 @@ function trimReconnectTextAlreadyRendered(
925
1047
  export function dedupeReconnectContentAgainstMessages(
926
1048
  content: ContentPart[],
927
1049
  messages: readonly unknown[],
928
- options?: { suppressToolRepeats?: boolean },
1050
+ options?: {
1051
+ suppressToolRepeats?: boolean;
1052
+ trimTailTextOverlap?: boolean;
1053
+ },
929
1054
  ): ContentPart[] {
930
1055
  if (content.length === 0) return content;
931
1056
  const snapshotDeduped = dedupePendingToolCallReplaysWithinContent(content);
1057
+ const reconnectPendingCounts = pendingToolCallCountsByName(snapshotDeduped);
932
1058
  let changed = snapshotDeduped !== content;
933
1059
  if (messages.length === 0) return changed ? snapshotDeduped : content;
934
1060
  const { byId, latestAssistantByFingerprint, latestAssistantByName } =
@@ -1009,6 +1135,31 @@ export function dedupeReconnectContentAgainstMessages(
1009
1135
  const latestByName = name
1010
1136
  ? latestAssistantByName.get(name)
1011
1137
  : undefined;
1138
+ const reconnectPendingCount = name
1139
+ ? (reconnectPendingCounts.get(name) ?? 0)
1140
+ : 0;
1141
+ const hasReaderLocalIdentity = Boolean(
1142
+ (id && toolCallIdIsReaderLocal(id)) ||
1143
+ (latestByName &&
1144
+ Array.from(latestByName.pendingIds).some(
1145
+ toolCallIdIsReaderLocal,
1146
+ )),
1147
+ );
1148
+ if (
1149
+ options?.suppressToolRepeats &&
1150
+ latestByName &&
1151
+ latestByName.pendingIds.size === 1 &&
1152
+ reconnectPendingCount === 1 &&
1153
+ hasReaderLocalIdentity &&
1154
+ latestByName.pendingRank >= reconnectRank
1155
+ ) {
1156
+ // During reconnect -> live-adapter handoff both accumulators own
1157
+ // the same run. Their pre-tool-start activity cards can have
1158
+ // unrelated reader-local ids and no args fingerprint, so name +
1159
+ // pending progress is the only stable identity available.
1160
+ changed = true;
1161
+ return false;
1162
+ }
1012
1163
  const matchesRenderedLocalId =
1013
1164
  id && latestByName
1014
1165
  ? Array.from(latestByName.ids).some((renderedId) =>
@@ -1037,6 +1188,7 @@ export function dedupeReconnectContentAgainstMessages(
1037
1188
  const textDeduped = trimReconnectTextAlreadyRendered(
1038
1189
  filtered,
1039
1190
  renderedAssistantText,
1191
+ { trimTailOverlap: options?.trimTailTextOverlap },
1040
1192
  );
1041
1193
  if (textDeduped !== filtered) changed = true;
1042
1194
  return changed ? textDeduped : content;
@@ -1151,19 +1303,40 @@ function contentHasVisibleReasoning(content: unknown): boolean {
1151
1303
  });
1152
1304
  }
1153
1305
 
1306
+ function contentHasUnresolvedToolCall(
1307
+ content: unknown,
1308
+ toolName?: string | null,
1309
+ ): boolean {
1310
+ if (!Array.isArray(content)) return false;
1311
+ return content.some((part) => {
1312
+ if (!part || typeof part !== "object") return false;
1313
+ const candidate = part as {
1314
+ type?: unknown;
1315
+ toolName?: unknown;
1316
+ result?: unknown;
1317
+ };
1318
+ return (
1319
+ candidate.type === "tool-call" &&
1320
+ candidate.result === undefined &&
1321
+ (!toolName || candidate.toolName === toolName)
1322
+ );
1323
+ });
1324
+ }
1325
+
1154
1326
  export function shouldShowGlobalRunningStatus({
1155
1327
  showRunningInUI,
1156
1328
  runningActivityLabel,
1329
+ runningActivityTool,
1157
1330
  latestMessage,
1158
1331
  reconnectContent,
1159
1332
  }: {
1160
1333
  showRunningInUI: boolean;
1161
1334
  runningActivityLabel: string | null | undefined;
1335
+ runningActivityTool?: string | null;
1162
1336
  latestMessage: unknown;
1163
1337
  reconnectContent: readonly ContentPart[];
1164
1338
  }): boolean {
1165
1339
  if (!showRunningInUI) return false;
1166
- if (runningActivityLabel) return true;
1167
1340
 
1168
1341
  const message =
1169
1342
  latestMessage && typeof latestMessage === "object"
@@ -1172,6 +1345,30 @@ export function shouldShowGlobalRunningStatus({
1172
1345
  const latestMessageHasReasoning =
1173
1346
  message?.role === "assistant" &&
1174
1347
  contentHasVisibleReasoning(message.content);
1348
+ const latestMessageHasUnresolvedTool =
1349
+ message?.role === "assistant" &&
1350
+ contentHasUnresolvedToolCall(message.content);
1351
+ const matchingActivityToolIsVisible = Boolean(
1352
+ runningActivityTool &&
1353
+ ((message?.role === "assistant" &&
1354
+ contentHasUnresolvedToolCall(message.content, runningActivityTool)) ||
1355
+ contentHasUnresolvedToolCall(reconnectContent, runningActivityTool)),
1356
+ );
1357
+
1358
+ // A pending card for the same tool is already the running indicator.
1359
+ // Rendering its global activity label as well shows the logical call twice
1360
+ // (for example, `generate design` plus `Writing generate design...`).
1361
+ if (runningActivityLabel && matchingActivityToolIsVisible) {
1362
+ return false;
1363
+ }
1364
+ if (
1365
+ !runningActivityLabel &&
1366
+ (latestMessageHasUnresolvedTool ||
1367
+ contentHasUnresolvedToolCall(reconnectContent))
1368
+ ) {
1369
+ return false;
1370
+ }
1371
+ if (runningActivityLabel) return true;
1175
1372
 
1176
1373
  return (
1177
1374
  !latestMessageHasReasoning && !contentHasVisibleReasoning(reconnectContent)
@@ -2041,6 +2238,10 @@ const AssistantChatInner = forwardRef<
2041
2238
  const reconnectCanMaterializeRef = useRef(false);
2042
2239
  const reconnectAbortRef = useRef<AbortController | null>(null);
2043
2240
  const reconnectAutoRecoveryCountRef = useRef(0);
2241
+ const reconnectOwnerMountedRef = useReconnectReaderOwner(
2242
+ reconnectRunIdRef,
2243
+ reconnectAbortRef,
2244
+ );
2044
2245
  const [pendingReconnectRecovery, setPendingReconnectRecovery] =
2045
2246
  useState<PendingReconnectRecovery | null>(null);
2046
2247
  // Nuclear stop: user clicked stop. Clears the stop button/indicator AND
@@ -2386,6 +2587,7 @@ const AssistantChatInner = forwardRef<
2386
2587
  const startReconnectToRun = useCallback(
2387
2588
  (runInfo: ActiveRunLookup): boolean => {
2388
2589
  if (
2590
+ !reconnectOwnerMountedRef.current ||
2389
2591
  !threadId ||
2390
2592
  !runInfo.runId ||
2391
2593
  (runInfo.status !== "running" && !isReplayableTerminalRun(runInfo))
@@ -2596,6 +2798,12 @@ const AssistantChatInner = forwardRef<
2596
2798
  rafPending = true;
2597
2799
  requestAnimationFrame(() => {
2598
2800
  rafPending = false;
2801
+ if (
2802
+ !reconnectOwnerMountedRef.current ||
2803
+ reconnectRunIdRef.current !== runId
2804
+ ) {
2805
+ return;
2806
+ }
2599
2807
  setReconnectContent(latestSnapshot);
2600
2808
  });
2601
2809
  };
@@ -2665,6 +2873,11 @@ const AssistantChatInner = forwardRef<
2665
2873
  clearInterval(idleCheck);
2666
2874
  }
2667
2875
 
2876
+ // A newer reader, live adapter, stop action, or component unmount took
2877
+ // ownership while this async loop was unwinding. Do not let the stale
2878
+ // generation refresh/import or update reconnect state afterwards.
2879
+ if (reconnectRunIdRef.current !== runId) return;
2880
+
2668
2881
  if (noProgressDuringReconnect && reconnectRunIdRef.current === runId) {
2669
2882
  const reconnectErrorCode =
2670
2883
  reconnectTerminalReason === "run_timeout"
@@ -3630,6 +3843,7 @@ const AssistantChatInner = forwardRef<
3630
3843
  const stillNeeded =
3631
3844
  dedupeReconnectContentAgainstMessages(reconnectContent, messages, {
3632
3845
  suppressToolRepeats: true,
3846
+ trimTailTextOverlap: true,
3633
3847
  }).length > 0;
3634
3848
  if (!stillNeeded) {
3635
3849
  setReconnectContent([]);
@@ -4236,7 +4450,11 @@ const AssistantChatInner = forwardRef<
4236
4450
  const visibleReconnectContent = dedupeReconnectContentAgainstMessages(
4237
4451
  reconnectContent,
4238
4452
  messages,
4239
- { suppressToolRepeats: adapterHandoffPending },
4453
+ {
4454
+ suppressToolRepeats: adapterHandoffPending,
4455
+ trimTailTextOverlap:
4456
+ adapterHandoffPending || reconnectTailOnlyRef.current,
4457
+ },
4240
4458
  );
4241
4459
  const chatScrollResetKey = `${tabId ?? ""}:${threadId ?? ""}`;
4242
4460
 
@@ -4262,11 +4480,18 @@ const AssistantChatInner = forwardRef<
4262
4480
  );
4263
4481
  const latestMessage = messages[messages.length - 1];
4264
4482
  const latestMessageRole = latestMessage?.role;
4483
+ const reconnectStatusContent =
4484
+ visibleReconnectContent.length > 0
4485
+ ? visibleReconnectContent
4486
+ : reconnectContent.length === 0
4487
+ ? reconnectActivityContent
4488
+ : [];
4265
4489
  const showGlobalRunningStatus = shouldShowGlobalRunningStatus({
4266
4490
  showRunningInUI,
4267
4491
  runningActivityLabel,
4492
+ runningActivityTool,
4268
4493
  latestMessage,
4269
- reconnectContent: visibleReconnectContent,
4494
+ reconnectContent: reconnectStatusContent,
4270
4495
  });
4271
4496
  const latestAssistantWasPlan =
4272
4497
  latestMessageRole === "assistant" &&
@@ -976,7 +976,7 @@ export function ReasoningCell({
976
976
  durationMs?: number | null;
977
977
  }) {
978
978
  const embeddedInWorkSummary = React.useContext(WorkSummaryContentContext);
979
- const [open, setOpen] = useState(defaultOpen ?? isStreaming);
979
+ const [open, setOpen] = useState(defaultOpen ?? true);
980
980
  const trimmed = text.trim();
981
981
  if (!trimmed && !isStreaming) return null;
982
982
 
@@ -0,0 +1,28 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ type MutableRef<T> = { current: T };
4
+
5
+ /**
6
+ * Owns the lifetime of AssistantChat's reconnect reader. The inner chat can be
7
+ * remounted while its outer runtime survives (error-boundary recovery, keyed
8
+ * thread changes), so unmount must explicitly retire the old reader.
9
+ */
10
+ export function useReconnectReaderOwner(
11
+ reconnectRunIdRef: MutableRef<string | null>,
12
+ reconnectAbortRef: MutableRef<AbortController | null>,
13
+ ): MutableRef<boolean> {
14
+ const mountedRef = useRef(false);
15
+
16
+ useEffect(() => {
17
+ mountedRef.current = true;
18
+ return () => {
19
+ mountedRef.current = false;
20
+ const reconnectAbort = reconnectAbortRef.current;
21
+ reconnectRunIdRef.current = null;
22
+ reconnectAbortRef.current = null;
23
+ reconnectAbort?.abort();
24
+ };
25
+ }, [reconnectAbortRef, reconnectRunIdRef]);
26
+
27
+ return mountedRef;
28
+ }
@@ -4,7 +4,11 @@ import {
4
4
  IconUpload,
5
5
  IconX,
6
6
  } from "@tabler/icons-react";
7
- import { useQuery, useQueryClient } from "@tanstack/react-query";
7
+ import {
8
+ keepPreviousData,
9
+ useQuery,
10
+ useQueryClient,
11
+ } from "@tanstack/react-query";
8
12
  import { useCallback, useEffect, useMemo, useState } from "react";
9
13
 
10
14
  import { sendToAgentChat } from "./agent-chat.js";
@@ -1051,6 +1055,10 @@ export function useGuidedQuestionFlow({
1051
1055
  },
1052
1056
  refetchInterval: resolvedRefetchInterval,
1053
1057
  structuralSharing: false,
1058
+ // A matching app-state event changes the query key. Preserve the existing
1059
+ // payload while the replacement read is in flight so full-canvas question
1060
+ // forms do not unmount and flash during routine sync updates.
1061
+ placeholderData: keepPreviousData,
1054
1062
  });
1055
1063
 
1056
1064
  useEffect(() => {
@@ -43,16 +43,15 @@ interface PlatformInfo {
43
43
  const PLATFORMS: PlatformInfo[] = [
44
44
  {
45
45
  id: "slack",
46
- label: "Slack",
46
+ label: "Slack (legacy)",
47
47
  icon: IconBrandSlack,
48
- description: "Message your agent from any Slack channel or DM.",
48
+ description:
49
+ "Legacy single-workspace setup. Use Settings → Messaging for new Slack connections.",
49
50
  envVars: ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"],
50
51
  setupSteps: [
51
- "Create a Slack app at api.slack.com/apps",
52
- 'Enable "Event Subscriptions" and point to your webhook URL',
53
- "Subscribe to message.im and app_mention events",
54
- "Install the app to your workspace",
55
- "Copy the Bot Token and Signing Secret into your environment",
52
+ "Open Settings Messaging for the supported managed Slack setup",
53
+ "Use this legacy setup only for an existing single-workspace installation",
54
+ "Managed OAuth stores workspace bot tokens automatically; do not add SLACK_BOT_TOKEN for new connections",
56
55
  ],
57
56
  docsUrl: "https://api.slack.com/apps",
58
57
  },
@@ -231,12 +231,6 @@ export type PreparingActionState = {
231
231
  toolEntries?: Map<string, PreparingActionEntry>;
232
232
  };
233
233
 
234
- function formatProgressBytes(bytes: number): string {
235
- if (bytes < 1024) return `${bytes} B`;
236
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
237
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
238
- }
239
-
240
234
  function activityProgressBytes(ev: SSEEvent): number | undefined {
241
235
  return typeof ev.progressBytes === "number" &&
242
236
  Number.isFinite(ev.progressBytes) &&
@@ -281,7 +275,7 @@ function preparationActivityLabel(
281
275
  if (progressBytes <= 0) {
282
276
  return `Preparing ${action}...`;
283
277
  }
284
- return `Writing ${action}... (${formatProgressBytes(progressBytes)} prepared)`;
278
+ return `Writing ${action}...`;
285
279
  }
286
280
 
287
281
  function visibleActivityLabel(ev: SSEEvent, tool?: string): string {
@@ -301,16 +295,8 @@ function findPendingToolCallIndex(
301
295
  // calls can be in flight simultaneously, and name-only matching would
302
296
  // attach a result to the wrong call.
303
297
  if (toolCallId) {
304
- for (let i = content.length - 1; i >= 0; i--) {
305
- const part = content[i];
306
- if (
307
- part.type === "tool-call" &&
308
- part.toolCallId === toolCallId &&
309
- part.result === undefined
310
- ) {
311
- return i;
312
- }
313
- }
298
+ const exactIndex = findPendingToolCallIndexById(content, toolCallId);
299
+ if (exactIndex >= 0) return exactIndex;
314
300
  // Fall through to name-matching: the start event may have arrived before
315
301
  // the server started emitting ids (e.g. older server build), so the
316
302
  // stored toolCallId is the locally-generated "tc_N" value rather than the
@@ -329,6 +315,76 @@ function findPendingToolCallIndex(
329
315
  return -1;
330
316
  }
331
317
 
318
+ function findPendingToolCallIndexById(
319
+ content: ContentPart[],
320
+ toolCallId: string,
321
+ ): number {
322
+ for (let i = content.length - 1; i >= 0; i--) {
323
+ const part = content[i];
324
+ if (
325
+ part.type === "tool-call" &&
326
+ part.toolCallId === toolCallId &&
327
+ part.result === undefined
328
+ ) {
329
+ return i;
330
+ }
331
+ }
332
+ return -1;
333
+ }
334
+
335
+ function findOldestPendingActivityToolCallIndex(
336
+ content: ContentPart[],
337
+ toolName: string,
338
+ ): number {
339
+ for (let i = 0; i < content.length; i += 1) {
340
+ const part = content[i];
341
+ if (
342
+ part.type === "tool-call" &&
343
+ part.toolName === toolName &&
344
+ part.activity === true &&
345
+ part.result === undefined
346
+ ) {
347
+ return i;
348
+ }
349
+ }
350
+ return -1;
351
+ }
352
+
353
+ function findPendingActivityToolCallIndex(
354
+ content: ContentPart[],
355
+ toolName: string,
356
+ toolCallId?: string,
357
+ ): number {
358
+ if (!toolCallId) {
359
+ return findPendingToolCallIndex(content, toolName);
360
+ }
361
+
362
+ // Activity events emitted while the model is assembling tool input already
363
+ // carry the engine's stable call id. Match that id exactly so repeated
364
+ // progress for one call updates one placeholder while parallel same-name
365
+ // calls retain separate cards.
366
+ const exactIndex = findPendingToolCallIndexById(content, toolCallId);
367
+ if (exactIndex >= 0) return exactIndex;
368
+
369
+ // Older activity events may omit the id before a later progress event adds
370
+ // it. Upgrade one unambiguous reader-local placeholder instead of painting a
371
+ // second card for the same logical call.
372
+ const readerLocalCandidates: number[] = [];
373
+ for (let i = 0; i < content.length; i += 1) {
374
+ const part = content[i];
375
+ if (
376
+ part.type === "tool-call" &&
377
+ part.toolName === toolName &&
378
+ part.activity === true &&
379
+ part.result === undefined &&
380
+ /^tc_\d+$/.test(part.toolCallId)
381
+ ) {
382
+ readerLocalCandidates.push(i);
383
+ }
384
+ }
385
+ return readerLocalCandidates.length === 1 ? readerLocalCandidates[0] : -1;
386
+ }
387
+
332
388
  function findCompletedToolCallIndex(
333
389
  content: ContentPart[],
334
390
  toolCallId?: string,
@@ -1000,7 +1056,17 @@ export function processEvent(
1000
1056
  }
1001
1057
  if (!tool) return { action: "continue" };
1002
1058
 
1003
- const pendingToolCallIndex = findPendingToolCallIndex(content, tool);
1059
+ const pendingToolCallIndex = findPendingActivityToolCallIndex(
1060
+ content,
1061
+ tool,
1062
+ ev.id,
1063
+ );
1064
+ if (pendingToolCallIndex >= 0 && ev.id) {
1065
+ const pending = content[pendingToolCallIndex];
1066
+ if (pending?.type === "tool-call" && pending.toolCallId !== ev.id) {
1067
+ content[pendingToolCallIndex] = { ...pending, toolCallId: ev.id };
1068
+ }
1069
+ }
1004
1070
  if (pendingToolCallIndex === -1) {
1005
1071
  // Only surface a placeholder spinner when this tool has no card yet. A
1006
1072
  // trailing activity heartbeat that arrives after the matching tool_done
@@ -1011,12 +1077,13 @@ export function processEvent(
1011
1077
  (part) =>
1012
1078
  part.type === "tool-call" &&
1013
1079
  part.toolName === tool &&
1014
- part.result !== undefined,
1080
+ part.result !== undefined &&
1081
+ (!ev.id || part.toolCallId === ev.id),
1015
1082
  );
1016
1083
  if (!hasCompletedSameTool) {
1017
1084
  content.push({
1018
1085
  type: "tool-call",
1019
- toolCallId: `tc_${++toolCallCounter.value}`,
1086
+ toolCallId: ev.id ?? `tc_${++toolCallCounter.value}`,
1020
1087
  toolName: tool,
1021
1088
  argsText: "",
1022
1089
  args: {},
@@ -1054,7 +1121,9 @@ export function processEvent(
1054
1121
  }
1055
1122
  // Pass the server-assigned id so we upgrade the pending activity card
1056
1123
  // using id-match when available (parallel same-name calls stay separate).
1057
- const pendingToolCallIndex = findPendingToolCallIndex(content, tool, ev.id);
1124
+ const pendingToolCallIndex = ev.id
1125
+ ? findPendingActivityToolCallIndex(content, tool, ev.id)
1126
+ : findOldestPendingActivityToolCallIndex(content, tool);
1058
1127
  const pendingToolCall =
1059
1128
  pendingToolCallIndex >= 0 ? content[pendingToolCallIndex] : undefined;
1060
1129
  const pendingIsActivityPlaceholder =
@@ -66,9 +66,9 @@ export const WORKSPACE_CONNECTION_PROVIDERS = [
66
66
  credentialKeys: [
67
67
  {
68
68
  key: "SLACK_BOT_TOKEN",
69
- label: "Slack bot token",
69
+ label: "Slack bot token (legacy)",
70
70
  description:
71
- "Bot token with the smallest channel and history scopes needed by the template.",
71
+ "Legacy single-workspace fallback. For new messaging automations, connect Slack from Settings Messaging instead.",
72
72
  required: true,
73
73
  },
74
74
  ],