@buoy-gg/ask-buoy 7.0.35 → 7.0.36

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 (217) hide show
  1. package/README.md +35 -8
  2. package/lib/commonjs/blocks/BlockRenderer.js +13 -1
  3. package/lib/commonjs/blocks/BlockRenderer.js.map +1 -1
  4. package/lib/commonjs/blocks/DataBlocks.js +126 -33
  5. package/lib/commonjs/blocks/DataBlocks.js.map +1 -1
  6. package/lib/commonjs/blocks/ExtraBlocks.js +12 -6
  7. package/lib/commonjs/blocks/ExtraBlocks.js.map +1 -1
  8. package/lib/commonjs/blocks/MediaBlocks.js +22 -6
  9. package/lib/commonjs/blocks/MediaBlocks.js.map +1 -1
  10. package/lib/commonjs/blocks/blockText.js +60 -0
  11. package/lib/commonjs/blocks/blockText.js.map +1 -0
  12. package/lib/commonjs/blocks/blockText.test.js +53 -0
  13. package/lib/commonjs/blocks/blockText.test.js.map +1 -0
  14. package/lib/commonjs/blocks/primitives.js +58 -2
  15. package/lib/commonjs/blocks/primitives.js.map +1 -1
  16. package/lib/commonjs/components/ApprovalCard.js +111 -7
  17. package/lib/commonjs/components/ApprovalCard.js.map +1 -1
  18. package/lib/commonjs/components/AskBuoyHeader.js +36 -4
  19. package/lib/commonjs/components/AskBuoyHeader.js.map +1 -1
  20. package/lib/commonjs/components/AskBuoyModal.js +160 -34
  21. package/lib/commonjs/components/AskBuoyModal.js.map +1 -1
  22. package/lib/commonjs/components/AskBuoySettings.js +50 -0
  23. package/lib/commonjs/components/AskBuoySettings.js.map +1 -1
  24. package/lib/commonjs/components/Composer.js +94 -14
  25. package/lib/commonjs/components/Composer.js.map +1 -1
  26. package/lib/commonjs/components/EmptyState.js +12 -5
  27. package/lib/commonjs/components/EmptyState.js.map +1 -1
  28. package/lib/commonjs/components/KeyboardPad.js +80 -0
  29. package/lib/commonjs/components/KeyboardPad.js.map +1 -0
  30. package/lib/commonjs/components/Markdown.js +148 -90
  31. package/lib/commonjs/components/Markdown.js.map +1 -1
  32. package/lib/commonjs/components/MessageList.js +339 -92
  33. package/lib/commonjs/components/MessageList.js.map +1 -1
  34. package/lib/commonjs/components/QueuedMessages.js +145 -0
  35. package/lib/commonjs/components/QueuedMessages.js.map +1 -0
  36. package/lib/commonjs/components/markdownParse.js +131 -0
  37. package/lib/commonjs/components/markdownParse.js.map +1 -0
  38. package/lib/commonjs/components/markdownParse.test.js +99 -0
  39. package/lib/commonjs/components/markdownParse.test.js.map +1 -0
  40. package/lib/commonjs/store/activityGroups.js +110 -0
  41. package/lib/commonjs/store/activityGroups.js.map +1 -0
  42. package/lib/commonjs/store/activityGroups.test.js +78 -0
  43. package/lib/commonjs/store/activityGroups.test.js.map +1 -0
  44. package/lib/commonjs/store/askBuoySettings.js +7 -2
  45. package/lib/commonjs/store/askBuoySettings.js.map +1 -1
  46. package/lib/commonjs/store/askBuoyStore.js +192 -10
  47. package/lib/commonjs/store/askBuoyStore.js.map +1 -1
  48. package/lib/commonjs/store/blockPlacement.js +64 -0
  49. package/lib/commonjs/store/blockPlacement.js.map +1 -0
  50. package/lib/commonjs/store/blockPlacement.test.js +123 -0
  51. package/lib/commonjs/store/blockPlacement.test.js.map +1 -0
  52. package/lib/commonjs/store/queue.test.js +150 -0
  53. package/lib/commonjs/store/queue.test.js.map +1 -0
  54. package/lib/commonjs/store/resetConversation.js +77 -0
  55. package/lib/commonjs/store/resetConversation.js.map +1 -0
  56. package/lib/commonjs/store/restorePending.test.js +63 -0
  57. package/lib/commonjs/store/restorePending.test.js.map +1 -0
  58. package/lib/commonjs/store/suggestions.js +54 -0
  59. package/lib/commonjs/store/suggestions.js.map +1 -0
  60. package/lib/commonjs/store/suggestions.test.js +58 -0
  61. package/lib/commonjs/store/suggestions.test.js.map +1 -0
  62. package/lib/commonjs/store/systemLine.test.js +77 -0
  63. package/lib/commonjs/store/systemLine.test.js.map +1 -0
  64. package/lib/commonjs/store/ticketPrompt.js +17 -0
  65. package/lib/commonjs/store/ticketPrompt.js.map +1 -0
  66. package/lib/commonjs/store/transcriptPersistence.js +92 -3
  67. package/lib/commonjs/store/transcriptPersistence.js.map +1 -1
  68. package/lib/commonjs/store/transcriptPersistence.test.js +148 -0
  69. package/lib/commonjs/store/transcriptPersistence.test.js.map +1 -1
  70. package/lib/commonjs/store/turnRendering.test.js +151 -0
  71. package/lib/commonjs/store/turnRendering.test.js.map +1 -0
  72. package/lib/commonjs/store/useAskBuoy.js +271 -49
  73. package/lib/commonjs/store/useAskBuoy.js.map +1 -1
  74. package/lib/commonjs/sync/askBuoySyncAdapter.js +25 -4
  75. package/lib/commonjs/sync/askBuoySyncAdapter.js.map +1 -1
  76. package/lib/module/blocks/BlockRenderer.js +13 -1
  77. package/lib/module/blocks/BlockRenderer.js.map +1 -1
  78. package/lib/module/blocks/DataBlocks.js +128 -35
  79. package/lib/module/blocks/DataBlocks.js.map +1 -1
  80. package/lib/module/blocks/ExtraBlocks.js +12 -6
  81. package/lib/module/blocks/ExtraBlocks.js.map +1 -1
  82. package/lib/module/blocks/MediaBlocks.js +22 -6
  83. package/lib/module/blocks/MediaBlocks.js.map +1 -1
  84. package/lib/module/blocks/blockText.js +55 -0
  85. package/lib/module/blocks/blockText.js.map +1 -0
  86. package/lib/module/blocks/blockText.test.js +53 -0
  87. package/lib/module/blocks/blockText.test.js.map +1 -0
  88. package/lib/module/blocks/primitives.js +58 -2
  89. package/lib/module/blocks/primitives.js.map +1 -1
  90. package/lib/module/components/ApprovalCard.js +113 -9
  91. package/lib/module/components/ApprovalCard.js.map +1 -1
  92. package/lib/module/components/AskBuoyHeader.js +37 -5
  93. package/lib/module/components/AskBuoyHeader.js.map +1 -1
  94. package/lib/module/components/AskBuoyModal.js +163 -37
  95. package/lib/module/components/AskBuoyModal.js.map +1 -1
  96. package/lib/module/components/AskBuoySettings.js +52 -2
  97. package/lib/module/components/AskBuoySettings.js.map +1 -1
  98. package/lib/module/components/Composer.js +95 -15
  99. package/lib/module/components/Composer.js.map +1 -1
  100. package/lib/module/components/EmptyState.js +12 -5
  101. package/lib/module/components/EmptyState.js.map +1 -1
  102. package/lib/module/components/KeyboardPad.js +75 -0
  103. package/lib/module/components/KeyboardPad.js.map +1 -0
  104. package/lib/module/components/Markdown.js +149 -90
  105. package/lib/module/components/Markdown.js.map +1 -1
  106. package/lib/module/components/MessageList.js +341 -94
  107. package/lib/module/components/MessageList.js.map +1 -1
  108. package/lib/module/components/QueuedMessages.js +140 -0
  109. package/lib/module/components/QueuedMessages.js.map +1 -0
  110. package/lib/module/components/markdownParse.js +125 -0
  111. package/lib/module/components/markdownParse.js.map +1 -0
  112. package/lib/module/components/markdownParse.test.js +98 -0
  113. package/lib/module/components/markdownParse.test.js.map +1 -0
  114. package/lib/module/store/activityGroups.js +103 -0
  115. package/lib/module/store/activityGroups.js.map +1 -0
  116. package/lib/module/store/activityGroups.test.js +77 -0
  117. package/lib/module/store/activityGroups.test.js.map +1 -0
  118. package/lib/module/store/askBuoySettings.js +7 -2
  119. package/lib/module/store/askBuoySettings.js.map +1 -1
  120. package/lib/module/store/askBuoyStore.js +190 -10
  121. package/lib/module/store/askBuoyStore.js.map +1 -1
  122. package/lib/module/store/blockPlacement.js +59 -0
  123. package/lib/module/store/blockPlacement.js.map +1 -0
  124. package/lib/module/store/blockPlacement.test.js +122 -0
  125. package/lib/module/store/blockPlacement.test.js.map +1 -0
  126. package/lib/module/store/queue.test.js +149 -0
  127. package/lib/module/store/queue.test.js.map +1 -0
  128. package/lib/module/store/resetConversation.js +72 -0
  129. package/lib/module/store/resetConversation.js.map +1 -0
  130. package/lib/module/store/restorePending.test.js +62 -0
  131. package/lib/module/store/restorePending.test.js.map +1 -0
  132. package/lib/module/store/suggestions.js +49 -0
  133. package/lib/module/store/suggestions.js.map +1 -0
  134. package/lib/module/store/suggestions.test.js +57 -0
  135. package/lib/module/store/suggestions.test.js.map +1 -0
  136. package/lib/module/store/systemLine.test.js +76 -0
  137. package/lib/module/store/systemLine.test.js.map +1 -0
  138. package/lib/module/store/ticketPrompt.js +13 -0
  139. package/lib/module/store/ticketPrompt.js.map +1 -0
  140. package/lib/module/store/transcriptPersistence.js +92 -3
  141. package/lib/module/store/transcriptPersistence.js.map +1 -1
  142. package/lib/module/store/transcriptPersistence.test.js +148 -0
  143. package/lib/module/store/transcriptPersistence.test.js.map +1 -1
  144. package/lib/module/store/turnRendering.test.js +150 -0
  145. package/lib/module/store/turnRendering.test.js.map +1 -0
  146. package/lib/module/store/useAskBuoy.js +275 -53
  147. package/lib/module/store/useAskBuoy.js.map +1 -1
  148. package/lib/module/sync/askBuoySyncAdapter.js +25 -4
  149. package/lib/module/sync/askBuoySyncAdapter.js.map +1 -1
  150. package/lib/typescript/blocks/BlockRenderer.d.ts.map +1 -1
  151. package/lib/typescript/blocks/DataBlocks.d.ts +11 -0
  152. package/lib/typescript/blocks/DataBlocks.d.ts.map +1 -1
  153. package/lib/typescript/blocks/ExtraBlocks.d.ts +18 -2
  154. package/lib/typescript/blocks/ExtraBlocks.d.ts.map +1 -1
  155. package/lib/typescript/blocks/MediaBlocks.d.ts.map +1 -1
  156. package/lib/typescript/blocks/blockText.d.ts +14 -0
  157. package/lib/typescript/blocks/blockText.d.ts.map +1 -0
  158. package/lib/typescript/blocks/blockText.test.d.ts +2 -0
  159. package/lib/typescript/blocks/blockText.test.d.ts.map +1 -0
  160. package/lib/typescript/blocks/primitives.d.ts +16 -1
  161. package/lib/typescript/blocks/primitives.d.ts.map +1 -1
  162. package/lib/typescript/components/ApprovalCard.d.ts.map +1 -1
  163. package/lib/typescript/components/AskBuoyHeader.d.ts +31 -1
  164. package/lib/typescript/components/AskBuoyHeader.d.ts.map +1 -1
  165. package/lib/typescript/components/AskBuoyModal.d.ts.map +1 -1
  166. package/lib/typescript/components/AskBuoySettings.d.ts.map +1 -1
  167. package/lib/typescript/components/Composer.d.ts +16 -1
  168. package/lib/typescript/components/Composer.d.ts.map +1 -1
  169. package/lib/typescript/components/EmptyState.d.ts +5 -1
  170. package/lib/typescript/components/EmptyState.d.ts.map +1 -1
  171. package/lib/typescript/components/KeyboardPad.d.ts +33 -0
  172. package/lib/typescript/components/KeyboardPad.d.ts.map +1 -0
  173. package/lib/typescript/components/Markdown.d.ts +0 -10
  174. package/lib/typescript/components/Markdown.d.ts.map +1 -1
  175. package/lib/typescript/components/MessageList.d.ts +7 -2
  176. package/lib/typescript/components/MessageList.d.ts.map +1 -1
  177. package/lib/typescript/components/QueuedMessages.d.ts +10 -0
  178. package/lib/typescript/components/QueuedMessages.d.ts.map +1 -0
  179. package/lib/typescript/components/markdownParse.d.ts +49 -0
  180. package/lib/typescript/components/markdownParse.d.ts.map +1 -0
  181. package/lib/typescript/components/markdownParse.test.d.ts +2 -0
  182. package/lib/typescript/components/markdownParse.test.d.ts.map +1 -0
  183. package/lib/typescript/store/activityGroups.d.ts +29 -0
  184. package/lib/typescript/store/activityGroups.d.ts.map +1 -0
  185. package/lib/typescript/store/activityGroups.test.d.ts +2 -0
  186. package/lib/typescript/store/activityGroups.test.d.ts.map +1 -0
  187. package/lib/typescript/store/askBuoySettings.d.ts +22 -0
  188. package/lib/typescript/store/askBuoySettings.d.ts.map +1 -1
  189. package/lib/typescript/store/askBuoyStore.d.ts +142 -5
  190. package/lib/typescript/store/askBuoyStore.d.ts.map +1 -1
  191. package/lib/typescript/store/blockPlacement.d.ts +30 -0
  192. package/lib/typescript/store/blockPlacement.d.ts.map +1 -0
  193. package/lib/typescript/store/blockPlacement.test.d.ts +2 -0
  194. package/lib/typescript/store/blockPlacement.test.d.ts.map +1 -0
  195. package/lib/typescript/store/queue.test.d.ts +2 -0
  196. package/lib/typescript/store/queue.test.d.ts.map +1 -0
  197. package/lib/typescript/store/resetConversation.d.ts +38 -0
  198. package/lib/typescript/store/resetConversation.d.ts.map +1 -0
  199. package/lib/typescript/store/restorePending.test.d.ts +2 -0
  200. package/lib/typescript/store/restorePending.test.d.ts.map +1 -0
  201. package/lib/typescript/store/suggestions.d.ts +32 -0
  202. package/lib/typescript/store/suggestions.d.ts.map +1 -0
  203. package/lib/typescript/store/suggestions.test.d.ts +2 -0
  204. package/lib/typescript/store/suggestions.test.d.ts.map +1 -0
  205. package/lib/typescript/store/systemLine.test.d.ts +2 -0
  206. package/lib/typescript/store/systemLine.test.d.ts.map +1 -0
  207. package/lib/typescript/store/ticketPrompt.d.ts +11 -0
  208. package/lib/typescript/store/ticketPrompt.d.ts.map +1 -0
  209. package/lib/typescript/store/transcriptPersistence.d.ts +41 -2
  210. package/lib/typescript/store/transcriptPersistence.d.ts.map +1 -1
  211. package/lib/typescript/store/turnRendering.test.d.ts +2 -0
  212. package/lib/typescript/store/turnRendering.test.d.ts.map +1 -0
  213. package/lib/typescript/store/useAskBuoy.d.ts +23 -1
  214. package/lib/typescript/store/useAskBuoy.d.ts.map +1 -1
  215. package/lib/typescript/sync/askBuoySyncAdapter.d.ts +26 -6
  216. package/lib/typescript/sync/askBuoySyncAdapter.d.ts.map +1 -1
  217. package/package.json +9 -9
@@ -24,12 +24,31 @@
24
24
  * The previous version showed the model-facing sentence cut off at two
25
25
  * lines with the result's JSON key names beside it, which was unreadable
26
26
  * AND uninformative.
27
+ * - What a READ step returned (the receipt — "10 cached queries", a store's
28
+ * key/values) lives INSIDE that step's row and shows only when the row is
29
+ * expanded. Rendered as a standalone card under the activity list it read
30
+ * as part of the answer: a question about one Pokémon produced a wall of
31
+ * ten cache entries that a non-technical tester had no way to relate to
32
+ * what they asked. The step row already says "10 items"; tapping it is the
33
+ * ask for the detail. Receipts of WRITES (a diff of what changed) are drawn
34
+ * in full — that is the thing the user must see without asking.
35
+ * - CARDS COME LAST, AFTER THE PROSE. A turn reads activity → explanation →
36
+ * the thing itself, which is the order a person would say it in. It used to
37
+ * be the other way round, and on a long turn the answer — a grid of 28 shop
38
+ * items — landed at the TOP with five paragraphs of narration underneath it,
39
+ * so the finished answer arrived before the sentence explaining it. Ask
40
+ * blocks (choice/confirm/input) stay last of all: they are the question the
41
+ * turn ends on.
27
42
  */
28
- import { useCallback, useEffect, useRef, useState } from "react";
43
+ import { memo, useCallback, useEffect, useRef, useState } from "react";
29
44
  import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from "react-native";
30
45
  import { AlertTriangle, Check, ChevronDown, ChevronRight, Copy, night, X } from "@buoy-gg/shared-ui";
31
- import { isAskBlock, toolTitle } from "@buoy-gg/agent-core";
46
+ import { toolTitle } from "@buoy-gg/agent-core";
32
47
  import { BlockRenderer } from "../blocks/BlockRenderer";
48
+ import { blocksToText } from "../blocks/blockText";
49
+ import { groupCalls, turnFooter } from "../store/activityGroups";
50
+ import { placeBlocks } from "../store/blockPlacement";
51
+ import { askBuoyStore } from "../store/askBuoyStore";
33
52
  import { Markdown } from "./Markdown";
34
53
  import { ThinkingStrip } from "./ThinkingStrip";
35
54
  import { TypingDots } from "./TypingDots";
@@ -70,6 +89,18 @@ function StatusMark({
70
89
  color: night.color.onAccent
71
90
  })
72
91
  });
92
+ // A grey ring with a bar — drawn, not an icon, so the shape is distinct
93
+ // from every outcome the MODEL produced. `declined` had no case at all:
94
+ // the row rendered a blank slot, and later nobody could tell whether the
95
+ // person or the policy had stopped it.
96
+ case "declined":
97
+ case "stopped":
98
+ return /*#__PURE__*/_jsx(View, {
99
+ style: [styles.mark, styles.markNotRun],
100
+ children: /*#__PURE__*/_jsx(View, {
101
+ style: styles.markNotRunBar
102
+ })
103
+ });
73
104
  }
74
105
  }
75
106
 
@@ -87,102 +118,161 @@ function statusText(call) {
87
118
  text: "Failed",
88
119
  tone: "danger"
89
120
  };
121
+ if (call.status === "declined") return {
122
+ text: "You declined",
123
+ tone: "muted"
124
+ };
125
+ if (call.status === "stopped") return {
126
+ text: "Not run — stopped",
127
+ tone: "muted"
128
+ };
90
129
  return {
91
130
  text: call.summary || "Done",
92
131
  tone: "muted"
93
132
  };
94
133
  }
134
+
135
+ /** Rows that never fold: anything that did not simply succeed. */
136
+ const isExceptional = c => c.status === "failed" || c.status === "refused" || c.status === "declined" || c.status === "stopped";
95
137
  function ActivityRow({
96
138
  call,
139
+ receipts,
97
140
  last
98
141
  }) {
99
142
  const [open, setOpen] = useState(false);
100
143
  const status = statusText(call);
101
144
  const Chevron = open ? ChevronDown : ChevronRight;
102
145
  const failureDetail = call.status === "failed" && call.summary && call.summary !== "Failed" ? call.summary : null;
103
- return /*#__PURE__*/_jsxs(Pressable, {
104
- testID: `buoy-ask-buoy-tool-${call.toolId}`,
105
- accessibilityRole: "button",
106
- accessibilityLabel: `${call.label}, ${status.text || "running"}`,
107
- accessibilityState: {
108
- expanded: open
109
- },
110
- onPress: () => setOpen(v => !v),
146
+ return /*#__PURE__*/_jsxs(View, {
111
147
  style: [styles.row, !last && styles.rowDivider],
112
- children: [/*#__PURE__*/_jsxs(View, {
113
- style: styles.rowLine,
114
- children: [/*#__PURE__*/_jsx(View, {
115
- style: styles.markSlot,
116
- children: /*#__PURE__*/_jsx(StatusMark, {
117
- status: call.status
118
- })
119
- }), /*#__PURE__*/_jsx(Text, {
120
- style: styles.rowTitle,
121
- children: call.label
122
- }), status.text ? /*#__PURE__*/_jsx(Text, {
123
- style: [styles.rowStatus, status.tone === "danger" && styles.rowStatusDanger, status.tone === "warning" && styles.rowStatusWarning],
124
- numberOfLines: 1,
125
- children: status.text
126
- }) : null, /*#__PURE__*/_jsx(Chevron, {
127
- size: 12,
128
- color: night.color.textTertiary
148
+ children: [/*#__PURE__*/_jsxs(Pressable, {
149
+ testID: `buoy-ask-buoy-tool-${call.toolId}`,
150
+ accessibilityRole: "button",
151
+ accessibilityLabel: `${call.label}, ${status.text || "running"}`,
152
+ accessibilityState: {
153
+ expanded: open
154
+ },
155
+ onPress: () => setOpen(v => !v),
156
+ children: [/*#__PURE__*/_jsxs(View, {
157
+ style: styles.rowLine,
158
+ children: [/*#__PURE__*/_jsx(View, {
159
+ style: styles.markSlot,
160
+ children: /*#__PURE__*/_jsx(StatusMark, {
161
+ status: call.status
162
+ })
163
+ }), /*#__PURE__*/_jsx(Text, {
164
+ style: styles.rowTitle,
165
+ children: call.label
166
+ }), status.text ? /*#__PURE__*/_jsx(Text, {
167
+ style: [styles.rowStatus, status.tone === "danger" && styles.rowStatusDanger, status.tone === "warning" && styles.rowStatusWarning],
168
+ numberOfLines: 1,
169
+ children: status.text
170
+ }) : null, /*#__PURE__*/_jsx(Chevron, {
171
+ size: 12,
172
+ color: night.color.textTertiary
173
+ })]
174
+ }), open && /*#__PURE__*/_jsxs(View, {
175
+ style: styles.rowDetail,
176
+ children: [/*#__PURE__*/_jsx(Text, {
177
+ style: styles.rowTool,
178
+ children: toolTitle(call.toolId).toUpperCase()
179
+ }), call.description ? /*#__PURE__*/_jsx(Text, {
180
+ style: styles.rowDescription,
181
+ children: call.description
182
+ }) : null, failureDetail ? /*#__PURE__*/_jsx(Text, {
183
+ style: styles.rowFailure,
184
+ children: failureDetail
185
+ }) : null]
129
186
  })]
130
- }), open && /*#__PURE__*/_jsxs(View, {
131
- style: styles.rowDetail,
132
- children: [/*#__PURE__*/_jsx(Text, {
133
- style: styles.rowTool,
134
- children: toolTitle(call.toolId).toUpperCase()
135
- }), call.description ? /*#__PURE__*/_jsx(Text, {
136
- style: styles.rowDescription,
137
- children: call.description
138
- }) : null, failureDetail ? /*#__PURE__*/_jsx(Text, {
139
- style: styles.rowFailure,
140
- children: failureDetail
141
- }) : null]
187
+ }), open && receipts.length > 0 && /*#__PURE__*/_jsx(View, {
188
+ style: styles.rowReceipts,
189
+ testID: `buoy-ask-buoy-tool-receipt-${call.toolId}`,
190
+ children: receipts.map(block => /*#__PURE__*/_jsx(BlockRenderer, {
191
+ block: block
192
+ }, block.id))
142
193
  })]
143
194
  });
144
195
  }
145
- const COLLAPSE_AFTER = 4;
146
196
 
147
197
  /**
148
- * More than four steps collapse to one line — "Did 15 things · 5 failed" —
149
- * with the failed and refused rows pinned so the bad news is never hidden.
198
+ * Consecutive successful reads fold into one row — "Looked at Network and
199
+ * Storage · 3 reads" — expandable to the individual rows. Everything else
200
+ * (running, failed, refused, declined, stopped, any write) is always its own
201
+ * row, and a group never crosses a write. See store/activityGroups.ts for the
202
+ * rule and why the old "Did 7 things" fold was replaced.
150
203
  */
151
- function ActivityCard({
152
- calls
204
+ function ReadsGroup({
205
+ label,
206
+ count,
207
+ calls,
208
+ receipts,
209
+ last
153
210
  }) {
154
- const [expanded, setExpanded] = useState(false);
155
- const running = calls.some(c => c.status === "running");
156
- const collapse = calls.length > COLLAPSE_AFTER && !expanded && !running;
157
- const failed = calls.filter(c => c.status === "failed").length;
158
- const refused = calls.filter(c => c.status === "refused").length;
159
- const shown = collapse ? calls.filter(c => c.status === "failed" || c.status === "refused") : calls;
211
+ const [open, setOpen] = useState(false);
212
+ const Chevron = open ? ChevronDown : ChevronRight;
160
213
  return /*#__PURE__*/_jsxs(View, {
161
- style: styles.card,
162
- testID: "buoy-ask-buoy-activity",
163
- children: [collapse && /*#__PURE__*/_jsx(Pressable, {
164
- onPress: () => setExpanded(true),
214
+ style: !last && !open && styles.rowDivider,
215
+ children: [/*#__PURE__*/_jsx(Pressable, {
216
+ testID: "buoy-ask-buoy-reads-group",
165
217
  accessibilityRole: "button",
166
- style: [styles.row, shown.length > 0 && styles.rowDivider],
167
- testID: "buoy-ask-buoy-activity-expand",
218
+ accessibilityLabel: `${label}, ${count} reads`,
219
+ accessibilityState: {
220
+ expanded: open
221
+ },
222
+ onPress: () => setOpen(v => !v),
223
+ style: [styles.row, open && styles.rowDivider],
168
224
  children: /*#__PURE__*/_jsxs(View, {
169
225
  style: styles.rowLine,
170
- children: [/*#__PURE__*/_jsxs(Text, {
226
+ children: [/*#__PURE__*/_jsx(View, {
227
+ style: styles.markSlot,
228
+ children: /*#__PURE__*/_jsx(StatusMark, {
229
+ status: "ok"
230
+ })
231
+ }), /*#__PURE__*/_jsx(Text, {
171
232
  style: styles.rowTitle,
172
- children: ["Did ", calls.length, " things", failed ? ` · ${failed} failed` : "", refused ? ` · ${refused} refused` : ""]
173
- }), /*#__PURE__*/_jsx(ChevronRight, {
233
+ children: label
234
+ }), /*#__PURE__*/_jsxs(Text, {
235
+ style: styles.rowStatus,
236
+ numberOfLines: 1,
237
+ children: [count, " reads"]
238
+ }), /*#__PURE__*/_jsx(Chevron, {
174
239
  size: 12,
175
240
  color: night.color.textTertiary
176
241
  })]
177
242
  })
178
- }), shown.map((c, i) => /*#__PURE__*/_jsx(ActivityRow, {
179
- call: c,
180
- last: i === shown.length - 1
181
- }, c.id))]
243
+ }), open && /*#__PURE__*/_jsx(View, {
244
+ style: styles.groupBody,
245
+ children: calls.map((c, i) => /*#__PURE__*/_jsx(ActivityRow, {
246
+ call: c,
247
+ receipts: receipts.get(c.id) ?? [],
248
+ last: last && i === calls.length - 1
249
+ }, c.id))
250
+ })]
251
+ });
252
+ }
253
+ function ActivityCard({
254
+ calls,
255
+ receipts
256
+ }) {
257
+ const items = groupCalls(calls);
258
+ return /*#__PURE__*/_jsx(View, {
259
+ style: styles.card,
260
+ testID: "buoy-ask-buoy-activity",
261
+ children: items.map((item, i) => item.kind === "reads" ? /*#__PURE__*/_jsx(ReadsGroup, {
262
+ label: item.label,
263
+ count: item.count,
264
+ calls: item.calls,
265
+ receipts: receipts,
266
+ last: i === items.length - 1
267
+ }, item.id) : /*#__PURE__*/_jsx(ActivityRow, {
268
+ call: item.call,
269
+ receipts: receipts.get(item.call.id) ?? [],
270
+ last: i === items.length - 1
271
+ }, item.call.id))
182
272
  });
183
273
  }
184
274
 
185
- /** Receipts and model blocks render between the activity card and the prose; Ask blocks last. */
275
+ /** Write receipts and model blocks render between the activity card and the prose; Ask blocks last. */
186
276
  function Blocks({
187
277
  blocks
188
278
  }) {
@@ -196,6 +286,20 @@ function Blocks({
196
286
  }, block.id))
197
287
  });
198
288
  }
289
+
290
+ /** A line from Buoy, not the model. Dim, centred, no footer, no copy pill. */
291
+ function SystemLine({
292
+ message
293
+ }) {
294
+ return /*#__PURE__*/_jsx(View, {
295
+ style: styles.systemLine,
296
+ testID: "buoy-ask-buoy-system-line",
297
+ children: /*#__PURE__*/_jsx(Text, {
298
+ style: styles.systemText,
299
+ children: message.text
300
+ })
301
+ });
302
+ }
199
303
  function UserMessage({
200
304
  message
201
305
  }) {
@@ -213,24 +317,43 @@ function UserMessage({
213
317
  }
214
318
  function AssistantMessage({
215
319
  message,
216
- onRetry
320
+ onRetry,
321
+ waitingOnUser
217
322
  }) {
218
323
  const {
219
324
  showThinking
220
325
  } = useAskBuoySettings();
221
- const running = message.toolCalls.some(c => c.status === "running");
222
- const waiting = !!message.streaming && message.text.length === 0 && !running;
223
- const askBlocks = message.blocks.filter(b => isAskBlock(b.block));
224
- const showBlocks = message.blocks.filter(b => !isAskBlock(b.block));
326
+ /**
327
+ * The dots run for the WHOLE turn, not just the wait for the first token.
328
+ *
329
+ * They used to hide as soon as any prose arrived and while a tool call was
330
+ * running. A turn that says "pulling the catalogue up — one moment" and
331
+ * then works for another minute is the normal shape of this tool, and it
332
+ * spent that minute with nothing moving at the bottom of the chat: the
333
+ * activity card had already collapsed to "Did 16 things", so the only clue
334
+ * anything was still happening was the Stop button and the header
335
+ * subtitle. The row spinner in the activity card says which STEP is
336
+ * running; this says the TURN is.
337
+ *
338
+ * The one state it stays out of is a turn waiting on the user — an
339
+ * approval or an Ask block. Nothing is loading there.
340
+ */
341
+ const waiting = !!message.streaming && !waitingOnUser;
342
+ const {
343
+ nested,
344
+ afterText,
345
+ ask: askBlocks
346
+ } = placeBlocks(message.blocks, message.toolCalls.map(c => c.id));
225
347
  return /*#__PURE__*/_jsxs(View, {
226
348
  style: styles.assistantWrap,
227
349
  children: [message.toolCalls.length > 0 && /*#__PURE__*/_jsx(ActivityCard, {
228
- calls: message.toolCalls
229
- }), /*#__PURE__*/_jsx(Blocks, {
230
- blocks: showBlocks
350
+ calls: message.toolCalls,
351
+ receipts: nested
231
352
  }), message.text.length > 0 && /*#__PURE__*/_jsx(Markdown, {
232
353
  text: message.text,
233
354
  style: styles.assistantText
355
+ }), /*#__PURE__*/_jsx(Blocks, {
356
+ blocks: afterText
234
357
  }), /*#__PURE__*/_jsx(Blocks, {
235
358
  blocks: askBlocks
236
359
  }), showThinking && !message.streaming && /*#__PURE__*/_jsx(ThinkingStrip, {
@@ -238,6 +361,45 @@ function AssistantMessage({
238
361
  }), waiting && /*#__PURE__*/_jsx(TypingDots, {}), message.error && /*#__PURE__*/_jsx(ErrorNote, {
239
362
  error: message.error,
240
363
  onRetry: onRetry
364
+ }), message.stopped && !message.error && /*#__PURE__*/_jsx(StoppedNote, {
365
+ onRetry: onRetry,
366
+ changed: message.toolCalls.some(c => c.status === "ok" && !!c.effect && c.effect !== "read")
367
+ })]
368
+ });
369
+ }
370
+
371
+ /**
372
+ * A turn the user stopped.
373
+ *
374
+ * Deliberately not the error box: they pressed the button. The whole point is
375
+ * that it is quiet — a muted line, the same Try-again affordance, and none of
376
+ * the red "check your connection" advice that a cancelled fetch used to
377
+ * produce. When a write had already landed before the tap, the line says so:
378
+ * Stop cannot pull a dispatched change back out of the app, and "Stopped."
379
+ * alone read as if nothing had happened.
380
+ */
381
+ function StoppedNote({
382
+ onRetry,
383
+ changed
384
+ }) {
385
+ return /*#__PURE__*/_jsxs(View, {
386
+ style: styles.stoppedBox,
387
+ testID: "buoy-ask-buoy-stopped",
388
+ children: [/*#__PURE__*/_jsx(Text, {
389
+ style: styles.stoppedText,
390
+ children: changed ? "Stopped. Changes already made are listed above." : "Stopped."
391
+ }), onRetry && /*#__PURE__*/_jsx(Pressable, {
392
+ testID: "buoy-ask-buoy-retry",
393
+ accessibilityRole: "button",
394
+ accessibilityLabel: "Try again",
395
+ onPress: onRetry,
396
+ style: ({
397
+ pressed
398
+ }) => [styles.retry, pressed && styles.retryPressed],
399
+ children: /*#__PURE__*/_jsx(Text, {
400
+ style: styles.retryText,
401
+ children: "Try again"
402
+ })
241
403
  })]
242
404
  });
243
405
  }
@@ -252,7 +414,7 @@ function ErrorNote({
252
414
  error,
253
415
  onRetry
254
416
  }) {
255
- const network = /fetch failed|network|ECONN|timed? ?out|socket|abort/i.test(error);
417
+ const network = /fetch failed|network|ECONN|timed? ?out|socket/i.test(error);
256
418
  const headline = network ? "Couldn't reach your AI endpoint. Check the connection and try again." : "Something went wrong with that turn.";
257
419
  return /*#__PURE__*/_jsxs(View, {
258
420
  style: styles.errorBox,
@@ -272,13 +434,14 @@ function ErrorNote({
272
434
  }), onRetry && /*#__PURE__*/_jsx(Pressable, {
273
435
  testID: "buoy-ask-buoy-retry",
274
436
  accessibilityRole: "button",
437
+ accessibilityLabel: "Try again",
275
438
  onPress: onRetry,
276
439
  style: ({
277
440
  pressed
278
441
  }) => [styles.retry, pressed && styles.retryPressed],
279
442
  children: /*#__PURE__*/_jsx(Text, {
280
443
  style: styles.retryText,
281
- children: "Retry"
444
+ children: "Try again"
282
445
  })
283
446
  })]
284
447
  })]
@@ -305,16 +468,24 @@ export function transcriptToText(messages, withTrace = false) {
305
468
  lines.push(`You: ${m.text}`);
306
469
  continue;
307
470
  }
471
+ if (m.system) {
472
+ lines.push(` — ${m.text}`);
473
+ continue;
474
+ }
308
475
  if (withTrace) {
309
476
  lines.push(...traceToText(m));
310
477
  } else {
311
478
  for (const c of m.toolCalls) {
312
- const status = c.status === "failed" || c.status === "refused" ? ` (${c.status})` : "";
479
+ const status = isExceptional(c) ? ` (${c.status})` : "";
313
480
  lines.push(` · ${c.label}${c.summary ? ` — ${c.summary}` : ""}${status}`);
314
481
  }
315
482
  }
316
483
  if (m.text) lines.push(`Ask Buoy: ${m.text}`);
484
+ // The cards, too. A finding is the thing a ticket wants and it used to be
485
+ // the one thing the paste left out.
486
+ for (const t of blocksToText(m.blocks.map(p => p.block))) lines.push(t);
317
487
  if (m.error) lines.push(` ! ${m.error}`);
488
+ if (m.stopped && !m.error) lines.push(" ! Stopped by the user.");
318
489
  }
319
490
  return lines.join("\n");
320
491
  }
@@ -351,54 +522,79 @@ function CopyPill({
351
522
  }
352
523
 
353
524
  /**
354
- * Under a finished answer: one Copy, and nothing else.
525
+ * Under a finished answer: the turn's footer and one Copy.
526
+ *
527
+ * The footer — "48s · 6 actions · 1 declined" — is always on. Duration and
528
+ * counts used to exist only inside the opt-in thinking strip, so nothing
529
+ * separated one turn from the next and "how long did that take?" had no
530
+ * answer without a setting. See `turnFooter` for what is counted and why.
355
531
  *
356
- * It is on EVERY answer rather than only the newest, because the moment you
532
+ * Copy is on EVERY answer rather than only the newest, because the moment you
357
533
  * decide to keep a conversation is usually a few replies after the one that
358
534
  * went wrong, and scrolling to the bottom to find the button is friction at
359
- * exactly the wrong time. Every one of them copies the WHOLE chat — one button
360
- * with one meaning beats four pills the reader has to choose between.
535
+ * exactly the wrong time. It copies the WHOLE chat, and now says so: labelled
536
+ * "Copy" it read as "copy this answer", and a paste into Slack brought the
537
+ * full transcript. The text is built on press from the live store, so this
538
+ * component takes no `messages` prop and a streaming answer no longer
539
+ * re-renders every finished one.
361
540
  */
362
541
  function TranscriptActions({
363
- messages
542
+ message
364
543
  }) {
365
544
  const {
366
545
  showThinking
367
546
  } = useAskBuoySettings();
368
- const getText = useCallback(() => transcriptToText(messages, showThinking), [messages, showThinking]);
369
- return /*#__PURE__*/_jsx(View, {
547
+ const getText = useCallback(() => transcriptToText(askBuoyStore.getState().messages, showThinking), [showThinking]);
548
+ const footer = turnFooter(message);
549
+ return /*#__PURE__*/_jsxs(View, {
370
550
  style: styles.transcriptActions,
371
- children: /*#__PURE__*/_jsx(CopyPill, {
551
+ children: [footer ? /*#__PURE__*/_jsx(Text, {
552
+ style: styles.turnFooter,
553
+ testID: "buoy-ask-buoy-turn-footer",
554
+ children: footer
555
+ }) : null, /*#__PURE__*/_jsx(CopyPill, {
372
556
  testID: "buoy-ask-buoy-copy",
373
- label: "Copy",
557
+ label: "Copy conversation",
374
558
  getText: getText
375
- })
559
+ })]
376
560
  });
377
561
  }
378
- function Row({
562
+
563
+ /**
564
+ * Memoised: a streaming answer changes the store on every token, and every
565
+ * row re-rendered for it. Props are the message itself (a new object when it
566
+ * changes), the retry handler (only the newest row gets one) and the waiting
567
+ * flag — nothing that changes when a DIFFERENT message streams.
568
+ */
569
+ const Row = /*#__PURE__*/memo(function Row({
379
570
  message,
380
- messages,
381
- onRetry
571
+ onRetry,
572
+ waitingOnUser
382
573
  }) {
383
574
  if (message.role === "user") return /*#__PURE__*/_jsx(UserMessage, {
384
575
  message: message
385
576
  });
577
+ if (message.system) return /*#__PURE__*/_jsx(SystemLine, {
578
+ message: message
579
+ });
386
580
  const finished = !message.streaming && !message.error;
387
581
  return /*#__PURE__*/_jsxs(View, {
388
582
  children: [/*#__PURE__*/_jsx(AssistantMessage, {
389
583
  message: message,
390
- onRetry: onRetry
584
+ onRetry: onRetry,
585
+ waitingOnUser: waitingOnUser
391
586
  }), finished && /*#__PURE__*/_jsx(TranscriptActions, {
392
- messages: messages
587
+ message: message
393
588
  })]
394
589
  });
395
- }
590
+ });
396
591
  export function MessageList({
397
592
  messages,
398
593
  ListHeaderComponent,
399
594
  ListEmptyComponent,
400
595
  ListFooterComponent,
401
- onRetry
596
+ onRetry,
597
+ waitingOnUser
402
598
  }) {
403
599
  const ref = useRef(null);
404
600
  // Only auto-scroll when the user is already at the bottom. Yanking them back
@@ -460,8 +656,8 @@ export function MessageList({
460
656
  item
461
657
  }) => /*#__PURE__*/_jsx(Row, {
462
658
  message: item,
463
- messages: messages,
464
- onRetry: item.id === last?.id ? onRetry : undefined
659
+ onRetry: item.id === last?.id ? onRetry : undefined,
660
+ waitingOnUser: item.id === last?.id ? waitingOnUser : false
465
661
  }),
466
662
  onScroll: onScroll,
467
663
  onScrollBeginDrag: onScrollBeginDrag,
@@ -510,6 +706,20 @@ const styles = StyleSheet.create({
510
706
  fontSize: night.font.row,
511
707
  lineHeight: 21
512
708
  },
709
+ // Buoy's own lines
710
+ systemLine: {
711
+ alignSelf: "stretch",
712
+ paddingHorizontal: 12,
713
+ paddingVertical: 7,
714
+ borderRadius: night.radius.row,
715
+ backgroundColor: night.color.fill
716
+ },
717
+ systemText: {
718
+ color: night.color.textSecondary,
719
+ fontSize: night.font.micro,
720
+ lineHeight: 16,
721
+ textAlign: "center"
722
+ },
513
723
  // Assistant
514
724
  assistantWrap: {
515
725
  alignItems: "stretch",
@@ -567,6 +777,16 @@ const styles = StyleSheet.create({
567
777
  markRefused: {
568
778
  backgroundColor: night.color.warning
569
779
  },
780
+ markNotRun: {
781
+ borderWidth: 1.5,
782
+ borderColor: night.color.textTertiary
783
+ },
784
+ markNotRunBar: {
785
+ width: 7,
786
+ height: 1.5,
787
+ borderRadius: 1,
788
+ backgroundColor: night.color.textSecondary
789
+ },
570
790
  rowTitle: {
571
791
  flex: 1,
572
792
  color: night.color.text,
@@ -605,6 +825,25 @@ const styles = StyleSheet.create({
605
825
  fontSize: night.font.caption,
606
826
  lineHeight: 18
607
827
  },
828
+ rowReceipts: {
829
+ paddingTop: 10,
830
+ gap: 10
831
+ },
832
+ groupBody: {
833
+ paddingLeft: 14,
834
+ backgroundColor: night.color.fill
835
+ },
836
+ stoppedBox: {
837
+ alignItems: "flex-start",
838
+ gap: 2,
839
+ paddingHorizontal: 12,
840
+ paddingVertical: 8
841
+ },
842
+ stoppedText: {
843
+ color: night.color.textTertiary,
844
+ fontSize: night.font.caption,
845
+ lineHeight: 18
846
+ },
608
847
  // Error
609
848
  errorBox: {
610
849
  flexDirection: "row",
@@ -648,8 +887,16 @@ const styles = StyleSheet.create({
648
887
  },
649
888
  transcriptActions: {
650
889
  flexDirection: "row",
890
+ alignItems: "center",
891
+ gap: 10,
651
892
  marginTop: 8
652
893
  },
894
+ turnFooter: {
895
+ flex: 1,
896
+ color: night.color.textTertiary,
897
+ fontSize: night.font.micro,
898
+ fontVariant: ["tabular-nums"]
899
+ },
653
900
  copyBtn: {
654
901
  paddingHorizontal: 10,
655
902
  paddingVertical: 5,