@astrosheep/square 0.4.0 → 0.4.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 (187) hide show
  1. package/dist/artifact.d.ts +1 -3
  2. package/dist/artifact.js +12 -36
  3. package/dist/attention-presentation.d.ts +2 -22
  4. package/dist/attention-presentation.js +9 -12
  5. package/dist/boundary-presentation.d.ts +3 -14
  6. package/dist/boundary-presentation.js +29 -104
  7. package/dist/card-presentation.d.ts +1 -0
  8. package/dist/card-presentation.js +2 -0
  9. package/dist/catch-decisions.js +1 -7
  10. package/dist/claude-delivery.d.ts +13 -8
  11. package/dist/claude-delivery.js +40 -93
  12. package/dist/claude-mcp-context.js +2 -2
  13. package/dist/claude-mod.js +28 -140
  14. package/dist/claude-transport.d.ts +3 -3
  15. package/dist/claude-transport.js +20 -44
  16. package/dist/cli/context.js +7 -9
  17. package/dist/cli/observation-commands.d.ts +2 -0
  18. package/dist/cli/observation-commands.js +12 -2
  19. package/dist/cli/registry.js +3 -1
  20. package/dist/codex-address.d.ts +2 -2
  21. package/dist/codex-boundary-state.d.ts +1 -5
  22. package/dist/codex-boundary-state.js +0 -4
  23. package/dist/codex-hook.d.ts +7 -10
  24. package/dist/codex-hook.js +69 -33
  25. package/dist/codex-queue.d.ts +10 -11
  26. package/dist/codex-queue.js +32 -32
  27. package/dist/codex-watch.d.ts +2 -0
  28. package/dist/codex-watch.js +100 -0
  29. package/dist/decisions.js +2 -2
  30. package/dist/delivery-deferral.d.ts +8 -17
  31. package/dist/delivery-deferral.js +0 -28
  32. package/dist/delivery-frame.d.ts +7 -0
  33. package/dist/delivery-frame.js +11 -0
  34. package/dist/delivery-ledger-sqlite.d.ts +97 -0
  35. package/dist/delivery-ledger-sqlite.js +496 -0
  36. package/dist/delivery-ledger.d.ts +196 -0
  37. package/dist/delivery-ledger.js +268 -0
  38. package/dist/delivery-lifetime.d.ts +6 -0
  39. package/dist/delivery-lifetime.js +45 -0
  40. package/dist/harness-links.js +61 -7
  41. package/dist/harness.js +12 -17
  42. package/dist/help.js +2 -0
  43. package/dist/http-delivery.d.ts +5 -9
  44. package/dist/http-delivery.js +20 -77
  45. package/dist/inbox.d.ts +6 -49
  46. package/dist/inbox.js +24 -371
  47. package/dist/index.d.ts +6 -3
  48. package/dist/index.js +4 -1
  49. package/dist/mcp.js +1 -1
  50. package/dist/model.d.ts +7 -47
  51. package/dist/model.js +1 -7
  52. package/dist/notifications.d.ts +24 -20
  53. package/dist/notifications.js +39 -108
  54. package/dist/operation-control.d.ts +4 -0
  55. package/dist/operation-control.js +20 -0
  56. package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
  57. package/dist/packages/agent-delivery/src/claude-native.js +5 -0
  58. package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
  59. package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
  60. package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
  61. package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
  62. package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
  63. package/dist/packages/agent-delivery/src/codex.js +41 -4
  64. package/dist/packages/agent-delivery/src/index.d.ts +3 -1
  65. package/dist/packages/agent-delivery/src/index.js +1 -1
  66. package/dist/packages/agent-delivery/src/paseo.js +11 -2
  67. package/dist/packages/agent-delivery/src/types.d.ts +1 -0
  68. package/dist/paseo-delivery.d.ts +5 -5
  69. package/dist/paseo-delivery.js +19 -97
  70. package/dist/paseo.d.ts +1 -1
  71. package/dist/paseo.js +1 -1
  72. package/dist/plugins/opencode/index.d.ts +5 -3
  73. package/dist/plugins/opencode/index.js +99 -21
  74. package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
  75. package/dist/plugins/opencode-shared/delivery.js +109 -0
  76. package/dist/plugins/opencode-v1/index.d.ts +28 -0
  77. package/dist/plugins/opencode-v1/index.js +41 -0
  78. package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
  79. package/dist/plugins/opencode-v1/runtime.js +152 -0
  80. package/dist/plugins/opencode-v1/transport.d.ts +13 -0
  81. package/dist/plugins/opencode-v1/transport.js +50 -0
  82. package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
  83. package/dist/plugins/paseo/shared/card-presentation.js +7 -0
  84. package/dist/ports.d.ts +31 -90
  85. package/dist/session-lifecycle.d.ts +8 -7
  86. package/dist/session-lifecycle.js +40 -42
  87. package/dist/sqlite-square.d.ts +14 -23
  88. package/dist/sqlite-square.js +132 -139
  89. package/dist/square-api.js +4 -7
  90. package/dist/square-core.d.ts +0 -1
  91. package/dist/square-domain.d.ts +5 -17
  92. package/dist/square-domain.js +104 -221
  93. package/dist/square-facade.d.ts +18 -15
  94. package/dist/square-post.d.ts +53 -0
  95. package/dist/square-post.js +4 -0
  96. package/dist/square-posting.d.ts +13 -0
  97. package/dist/square-posting.js +174 -0
  98. package/dist/square-producer-sqlite.d.ts +11 -0
  99. package/dist/square-producer-sqlite.js +210 -0
  100. package/dist/square-sqlite.d.ts +15 -25
  101. package/dist/square-sqlite.js +150 -226
  102. package/dist/views.d.ts +2 -2
  103. package/dist/views.js +0 -1
  104. package/dist/wake-sink.d.ts +4 -11
  105. package/dist/wake-sink.js +8 -32
  106. package/dist/wake-transports.d.ts +4 -7
  107. package/dist/wake-transports.js +8 -39
  108. package/examples/symmetric-domains.mjs +29 -249
  109. package/guides/change-waits.md +13 -53
  110. package/guides/claude-native.md +19 -175
  111. package/guides/codex-delivery.md +55 -80
  112. package/guides/delivery-primitives.md +48 -207
  113. package/guides/operation-cancellation.md +2 -5
  114. package/guides/participant-ownership.md +25 -89
  115. package/guides/pi-native-delivery.md +19 -56
  116. package/guides/pi-tools.md +6 -8
  117. package/guides/square-api.md +113 -615
  118. package/package.json +8 -8
  119. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  120. package/plugins/claude/hooks/hooks.json +1 -1
  121. package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
  122. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  123. package/plugins/codex/hooks/hooks.json +11 -23
  124. package/plugins/opencode/index.ts +91 -23
  125. package/plugins/opencode-shared/delivery.ts +91 -0
  126. package/plugins/opencode-v1/index.ts +36 -0
  127. package/plugins/opencode-v1/runtime.ts +134 -0
  128. package/plugins/opencode-v1/transport.ts +41 -0
  129. package/plugins/paseo/OVERVIEW.md +5 -5
  130. package/plugins/paseo/README.md +7 -6
  131. package/plugins/paseo/client/markdown.tsx +5 -5
  132. package/plugins/paseo/client/square-message.tsx +28 -24
  133. package/plugins/paseo/client/transform.ts +8 -5
  134. package/plugins/paseo/shared/card-presentation.ts +16 -0
  135. package/plugins/paseo/shared/message.ts +6 -11
  136. package/plugins/pi/square-pi.js +156 -417
  137. package/dist/channel-artifact.d.ts +0 -10
  138. package/dist/channel-artifact.js +0 -55
  139. package/dist/channel-delivery.d.ts +0 -11
  140. package/dist/channel-delivery.js +0 -51
  141. package/dist/channel-domain.d.ts +0 -3
  142. package/dist/channel-domain.js +0 -83
  143. package/dist/channel-facade.d.ts +0 -76
  144. package/dist/channel-facade.js +0 -1
  145. package/dist/channel-internal.d.ts +0 -126
  146. package/dist/channel-internal.js +0 -440
  147. package/dist/channel-metadata.d.ts +0 -11
  148. package/dist/channel-metadata.js +0 -1
  149. package/dist/channel-ports.d.ts +0 -241
  150. package/dist/channel-ports.js +0 -1
  151. package/dist/channel-presentation.d.ts +0 -17
  152. package/dist/channel-presentation.js +0 -22
  153. package/dist/channel-sqlite-schema.d.ts +0 -13
  154. package/dist/channel-sqlite-schema.js +0 -81
  155. package/dist/channel-transport.d.ts +0 -62
  156. package/dist/channel-transport.js +0 -1
  157. package/dist/channel.d.ts +0 -84
  158. package/dist/channel.js +0 -60
  159. package/dist/claude-delivery-status.d.ts +0 -13
  160. package/dist/claude-delivery-status.js +0 -46
  161. package/dist/cmd/notify-once.d.ts +0 -2
  162. package/dist/cmd/notify-once.js +0 -29
  163. package/dist/delivery-attempts.d.ts +0 -52
  164. package/dist/delivery-attempts.js +0 -73
  165. package/dist/delivery-effects.d.ts +0 -139
  166. package/dist/delivery-effects.js +0 -163
  167. package/dist/delivery-health.d.ts +0 -51
  168. package/dist/delivery-health.js +0 -173
  169. package/dist/delivery-operations.d.ts +0 -22
  170. package/dist/delivery-operations.js +0 -198
  171. package/dist/delivery-primitives.d.ts +0 -5
  172. package/dist/delivery-primitives.js +0 -3
  173. package/dist/delivery.d.ts +0 -56
  174. package/dist/delivery.js +0 -80
  175. package/dist/plugins/opencode/delivery.d.ts +0 -19
  176. package/dist/plugins/opencode/delivery.js +0 -223
  177. package/dist/presentation-operations.d.ts +0 -5
  178. package/dist/presentation-operations.js +0 -86
  179. package/dist/sqlite-channel.d.ts +0 -15
  180. package/dist/sqlite-channel.js +0 -600
  181. package/dist/square-index.d.ts +0 -16
  182. package/dist/square-index.js +0 -106
  183. package/dist/wake-preview.d.ts +0 -7
  184. package/dist/wake-preview.js +0 -15
  185. package/plugins/opencode/delivery.ts +0 -198
  186. /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
  187. /package/dist/{channel-values.js → square-values.js} +0 -0
@@ -5,10 +5,10 @@ who reached you and where. Expand it to see the original custom-message content,
5
5
  select text, or copy it in full. The card follows the active Paseo theme on
6
6
  desktop and mobile.
7
7
 
8
- Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Square
9
- 0.3.69 or newer supplies structured card headings. Older messages and joining
10
- context use a neutral “Square context” heading.
8
+ Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Structured messages
9
+ carry producer-neutral origin, author, reference, badge and full body details. Plain messages and
10
+ joining context use a neutral notification heading.
11
11
 
12
12
  The plugin changes presentation only. It does not send prompts or alter model
13
- context. If Square clipped a body before delivery, expansion shows that same
14
- preview and the command for reading the rest, not an additional fetch.
13
+ context. Expansion shows the delivered full display body, within a bounded preview,
14
+ while model-facing text stays clipped as before. No producer storage fetch is needed.
@@ -16,8 +16,9 @@ Paseo supplies all runtime modules. No Git preparation step is needed. Plugins
16
16
  are trusted, unsandboxed code running inside the Paseo app.
17
17
 
18
18
  Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Structured notification
19
- cards use Square's `details.items`, including each item's `actor`, `act`, `body` and
20
- `route`. Missing or malformed details fall back to the original plain text.
19
+ cards use `details.items` with producer-neutral presentation facts: `kind: 'presentation'`, `origin`,
20
+ optional `author`, `ref` and `badge`, and the full Markdown `body`. Missing or malformed details fall
21
+ back to the original plain text.
21
22
 
22
23
  ## Display
23
24
 
@@ -27,9 +28,9 @@ and unrecognized rows remain unchanged.
27
28
 
28
29
  The heading and expanded messages use `metadata.details.items` directly, never
29
30
  reverse-parsing notification envelopes or body text for senders, IDs or message
30
- boundaries. Each expanded item keeps its full source body, author, exact activity ID
31
- and bell marker, even when the model preview was clipped or omitted. IDs with gaps
32
- are not compressed into a misleading range; items from different squares remain
31
+ boundaries. Each expanded item keeps its full source body, author, exact reference
32
+ and badge, even when the model preview was clipped or omitted. References with gaps
33
+ are not compressed into a misleading range; items from different origins remain
33
34
  separate. Collapsing shows only the last item's preview. Body text containing square
34
35
  tags is ordinary content, not another notification.
35
36
 
@@ -44,7 +45,7 @@ Long content scrolls inside a bounded panel. The
44
45
  card does not fetch extra activity or mark anything seen. Live events and fetched history use
45
46
  the same transformer.
46
47
 
47
- An accent glyph, actor and square name identify the card. The header and its arrow
48
+ An accent glyph, author and origin label identify the card. The header and its arrow
48
49
  toggle an inline drawer. It uses Paseo theme colors, selectable text, accessible
49
50
  expansion state and compact layout on narrow displays. Removing the plugin restores
50
51
  Paseo's ordinary custom-message row.
@@ -166,14 +166,14 @@ export function SquareCue({ label, theme }: { label: ReactNode; theme: PluginThe
166
166
  }
167
167
 
168
168
  /**
169
- * A body of Markdown: no envelope, no route line — the words only, at full width.
170
- * `lead` runs the speaker into the first line; `actor` names whoever performs an action.
169
+ * A body of Markdown: no envelope — the words only, at full width.
170
+ * `lead` runs the author into the first line; `author` names whoever performs an action.
171
171
  */
172
- export function SquareMarkdownBody({ text, theme, lead, actor }: {
172
+ export function SquareMarkdownBody({ text, theme, lead, author }: {
173
173
  text: string;
174
174
  theme: PluginTheme;
175
175
  lead?: string;
176
- actor?: string;
176
+ author?: string;
177
177
  }) {
178
178
  const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
179
179
  const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
@@ -182,7 +182,7 @@ export function SquareMarkdownBody({ text, theme, lead, actor }: {
182
182
  const key = `body-${index}`;
183
183
  if (isAction(token)) {
184
184
  const words = renderInlines(children(children(token)[0]!), styles, key);
185
- return <SquareCue key={key} theme={theme} label={actor === undefined ? words : <>{actor} {words}</>} />;
185
+ return <SquareCue key={key} theme={theme} label={author === undefined ? words : <>{author} {words}</>} />;
186
186
  }
187
187
  if (pending === undefined) return renderBlocks([token], styles, key);
188
188
  const name = pending;
@@ -2,17 +2,17 @@ import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
2
  import { Icon } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMemo, useState } from "react";
4
4
  import { Pressable, ScrollView, Text, View } from "react-native";
5
- import type { SquareMessageData } from "../shared/message";
5
+ import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, type SquareMessageData } from "../shared/message";
6
6
  import { MONO, SquareCue, SquareInline, SquareMarkdownBody } from "./markdown";
7
7
  import { squarePalette } from "./palette";
8
8
 
9
9
  /**
10
10
  * A matte slate slip in the timeline's shared 4px-corner family: square seal ■,
11
11
  * thin quote guide under the header.
12
- * Header (both states): ■ square · who spoke … act ids · how many.
12
+ * Header (both states): ■ origin · authors … refs · how many.
13
13
  * Collapsed: two preview lines. Expanded: the whole scene, at full width.
14
- * Each structured message keeps its own sender, activity id and body.
15
- * A bell or an *action* is stage direction, a caption on a hairline.
14
+ * Each structured message keeps its own author, reference and body.
15
+ * A badge or an *action* is stage direction, a caption on a hairline.
16
16
  */
17
17
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
18
18
  const [expanded, setExpanded] = useState(false);
@@ -20,18 +20,20 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
20
20
  const palette = useMemo(() => squarePalette(theme), [theme]);
21
21
  const items = "items" in item.data ? item.data.items : undefined;
22
22
  const text = "text" in item.data ? item.data.text : undefined;
23
+ const presentations = items?.filter(entry => "origin" in entry);
23
24
  const latest = items?.at(-1);
24
25
  // Keep the source's own line breaks: collapsed and expanded text must wrap alike.
25
- const preview = latest?.preview ?? text;
26
- const speakers = [...new Set(items?.map(entry => entry.actor))];
27
- const chorus = speakers.length > 1;
28
- const squares = [...new Set(items?.map(entry => entry.square))];
29
- const squareName = squares.join(", ") || "Square";
30
- const bell = latest?.route === "bell";
31
- // Exact IDs never imply intervening activities, and different squares keep their own IDs.
32
- const actIds = items?.map(entry => entry.act) ?? [];
33
- const trailing = [items?.map(entry => squares.length > 1 ? `${entry.square} ${entry.act}` : entry.act).join(", "),
34
- items && items.length > 1 ? String(items.length) : undefined].filter(Boolean).join(" · ");
26
+ const preview = latest && "text" in latest ? outputPreview(latest.text, COLLAPSED_PREVIEW_MAX_CHARS)
27
+ : latest?.preview ?? text;
28
+ const authors = [...new Set(presentations?.flatMap(entry => entry.author === undefined ? [] : [entry.author]))];
29
+ const chorus = authors.length > 1;
30
+ const origins = [...new Set(presentations?.map(entry => entry.origin))];
31
+ const originName = origins.join(", ") || "Notification";
32
+ const badge = presentations?.at(-1)?.badge;
33
+ // Exact references never imply intervening messages; each origin keeps its own references.
34
+ const trailing = [presentations?.flatMap(entry => entry.ref === undefined ? []
35
+ : [origins.length > 1 ? `${entry.origin} ${entry.ref}` : entry.ref]).join(", "),
36
+ presentations && presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(" · ");
35
37
  const { accent, border, foreground, foregroundMuted: muted, surface1 } = palette.theme.colors;
36
38
  // On web the header brightens its details under the cursor; touch keeps them quiet.
37
39
  const detail = layout.platform === "web" && hovered ? foreground : muted;
@@ -45,7 +47,7 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
45
47
  ]}>
46
48
  <Pressable
47
49
  accessibilityRole="button"
48
- accessibilityLabel={`${expanded ? "收起" : "展开"} ${squareName}: ${speakers.join(", ") || "Square"}`}
50
+ accessibilityLabel={`${expanded ? "收起" : "展开"} ${originName}: ${authors.join(", ") || "Notification"}`}
49
51
  aria-expanded={expanded}
50
52
  onPress={() => setExpanded(value => !value)}
51
53
  onHoverIn={() => setHovered(true)}
@@ -55,9 +57,9 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
55
57
  <View style={row}>
56
58
  <View style={{ width: 6, height: 6, borderRadius: palette.seal.borderRadius, backgroundColor: accent }} />
57
59
  <Text numberOfLines={1} style={{ color: foreground, fontSize: 11.5, lineHeight: 17, fontWeight: "600", flex: 1 }}>
58
- {squareName}
59
- {(speakers.length > 0 || bell) && (
60
- <Text style={{ color: muted, fontWeight: "400", fontSize: 10.5 }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
60
+ {originName}
61
+ {(authors.length > 0 || badge) && (
62
+ <Text style={{ color: muted, fontWeight: "400", fontSize: 10.5 }}> · {[authors.join(", "), badge].filter(Boolean).join(" · ")}</Text>
61
63
  )}
62
64
  </Text>
63
65
  {trailing !== "" && <Text numberOfLines={1} style={{ color: detail, fontSize: 9.5, fontFamily: MONO, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{trailing}</Text>}
@@ -65,19 +67,21 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
65
67
  </View>
66
68
  {!expanded && preview !== undefined && preview !== "" && (
67
69
  <View style={{ ...indent, marginTop: 5, marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor }}>
68
- <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus ? latest?.actor : undefined} />
70
+ <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus && latest && "author" in latest ? latest.author : undefined} />
69
71
  </View>
70
72
  )}
71
73
  </Pressable>
72
74
  {expanded && (
73
75
  <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 3 }} nestedScrollEnabled>
74
76
  <View style={{ ...indent, gap: 7 }}>
75
- {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
77
+ {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => "text" in entry ? (
78
+ <SquareMarkdownBody key={index} text={entry.text} theme={palette.theme} />
79
+ ) : (
76
80
  <View key={index} style={{ gap: 6 }}>
77
- {entry.route === "bell"
78
- ? <SquareCue theme={palette.theme} label={`${entry.actor} 🔔 · ${entry.square} · ${entry.act}`} />
79
- : <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{entry.square} · {entry.act}</Text>}
80
- <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.actor : undefined} actor={entry.actor} />
81
+ {entry.badge
82
+ ? <SquareCue theme={palette.theme} label={[[entry.author, entry.badge].filter(Boolean).join(" "), entry.origin, entry.ref].filter(Boolean).join(" · ")} />
83
+ : <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{[entry.origin, entry.ref].filter(Boolean).join(" · ")}</Text>}
84
+ <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} author={entry.author} />
81
85
  </View>
82
86
  ))}
83
87
  </View>
@@ -13,13 +13,16 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
13
13
  let data: SquareMessageData;
14
14
  if (details.success) {
15
15
  const items = details.data.items, latest = items.at(-1)!;
16
- const preview = outputPreview(latest.preview ?? latest.body, COLLAPSED_PREVIEW_MAX_CHARS);
16
+ const preview = outputPreview("text" in latest ? latest.text : latest.preview ?? latest.body, COLLAPSED_PREVIEW_MAX_CHARS);
17
17
  let remaining = OUTPUT_DISPLAY_MAX_CHARS - preview.length;
18
- const clipped = items.reduce((sum, entry) => sum + entry.body.length, 0) > remaining;
19
- data = { items: items.map(({ preview: _preview, ...entry }, index) => {
20
- const body = clipped ? outputPreview(entry.body, Math.floor(remaining / (items.length - index))) : entry.body;
18
+ const clipped = items.reduce((sum, entry) => sum + ("text" in entry ? entry.text : entry.body).length, 0) > remaining;
19
+ data = { items: items.map((entry, index) => {
20
+ const source = "text" in entry ? entry.text : entry.body;
21
+ const body = clipped ? outputPreview(source, Math.floor(remaining / (items.length - index))) : source;
21
22
  remaining -= body.length;
22
- return { ...entry, body, ...(index === items.length - 1 ? { preview } : {}) };
23
+ if ("text" in entry) return { text: body };
24
+ const { preview: _preview, ...presentation } = entry;
25
+ return { ...presentation, body, ...(index === items.length - 1 ? { preview } : {}) };
23
26
  }) };
24
27
  } else {
25
28
  data = { text: outputPreview(item.detail.text) };
@@ -0,0 +1,16 @@
1
+ /** Producer-neutral display facts; independent of model-facing delivery text. */
2
+ export type CardPresentation = {
3
+ readonly kind: 'presentation';
4
+ readonly origin: string;
5
+ readonly author?: string;
6
+ readonly ref?: string;
7
+ readonly badge?: string;
8
+ readonly body: string;
9
+ };
10
+
11
+ export function isCardPresentation(value: unknown): value is CardPresentation {
12
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
13
+ const item = value as Record<string, unknown>;
14
+ return item.kind === 'presentation' && typeof item.origin === 'string' && typeof item.body === 'string'
15
+ && ['author', 'ref', 'badge'].every(key => item[key] === undefined || typeof item[key] === 'string');
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { isCardPresentation, type CardPresentation } from "./card-presentation.ts";
2
3
 
3
4
  /** Aggregate client display budget, including omission lines; source history stays unchanged. */
4
5
  export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
@@ -15,17 +16,11 @@ export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS)
15
16
  return head + middle + tail;
16
17
  }
17
18
 
18
- /** Renderer facts emitted by pendingCardDetails, never derived from body or route prose. */
19
+ /** Receiver display facts, with a bounded preview derived on the client. */
20
+ const presentationItemSchema = z.custom<CardPresentation>(isCardPresentation)
21
+ .and(z.object({ preview: z.string().optional() }));
19
22
  export const squareCardDetailsSchema = z.object({
20
- items: z.array(z.object({
21
- square: z.string(),
22
- actor: z.string(),
23
- act: z.string().regex(/^act\/(0|[1-9]\d*)$/),
24
- body: z.string(),
25
- recipient: z.string().optional(),
26
- route: z.enum(["mention", "reply", "attention", "bell"]),
27
- preview: z.string().optional(),
28
- })).min(1),
23
+ items: z.array(z.union([presentationItemSchema, z.object({ text: z.string() })])).min(1),
29
24
  });
30
25
 
31
26
  export const squareMessageSchema = z.union([
@@ -33,4 +28,4 @@ export const squareMessageSchema = z.union([
33
28
  z.object({ text: z.string() }),
34
29
  ]);
35
30
  export type SquareMessageData = z.output<typeof squareMessageSchema>;
36
- export type SquareNotificationItem = z.output<typeof squareCardDetailsSchema>["items"][number];
31
+ export type PresentationItem = z.output<typeof presentationItemSchema>;