@astrosheep/square 0.3.73 → 0.3.75

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 (49) hide show
  1. package/dist/attention-presentation.d.ts +2 -0
  2. package/dist/attention-presentation.js +11 -2
  3. package/dist/boundary-presentation.d.ts +8 -1
  4. package/dist/boundary-presentation.js +8 -1
  5. package/dist/cli/context.d.ts +1 -1
  6. package/dist/cli/express-presentation.js +1 -2
  7. package/dist/cli/ledger-command.js +6 -2
  8. package/dist/cli/meta-commands.js +1 -1
  9. package/dist/decisions.js +6 -21
  10. package/dist/help.js +4 -4
  11. package/dist/host-ledger-migration.d.ts +1 -0
  12. package/dist/host-ledger-migration.js +62 -21
  13. package/dist/host-ledger-sqlite.d.ts +4 -1
  14. package/dist/host-ledger-sqlite.js +36 -9
  15. package/dist/inbox.d.ts +2 -0
  16. package/dist/inbox.js +64 -22
  17. package/dist/mcp.d.ts +2 -1
  18. package/dist/mcp.js +51 -26
  19. package/dist/model.d.ts +2 -3
  20. package/dist/model.js +5 -2
  21. package/dist/open-square.d.ts +12 -2
  22. package/dist/open-square.js +42 -4
  23. package/dist/participant-host.js +6 -2
  24. package/dist/ports.d.ts +10 -0
  25. package/dist/presentation.js +1 -1
  26. package/dist/square-actions.d.ts +3 -1
  27. package/dist/square-actions.js +10 -5
  28. package/dist/square-application.d.ts +4 -0
  29. package/dist/square-application.js +42 -28
  30. package/dist/square-facade.d.ts +26 -22
  31. package/dist/views.d.ts +3 -1
  32. package/dist/views.js +37 -29
  33. package/guides/application-demand-optimization.md +50 -0
  34. package/guides/catch-cold-path-optimization.md +56 -0
  35. package/guides/catch-lock-investigation.md +81 -0
  36. package/guides/host-ledger.md +2 -2
  37. package/guides/mcp-connection-lifetime.md +22 -0
  38. package/guides/pi-boundary-pending-reuse.md +13 -0
  39. package/package.json +2 -1
  40. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  41. package/plugins/claude/skills/square/SKILL.md +1 -1
  42. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  43. package/plugins/paseo/README.md +10 -5
  44. package/plugins/paseo/client/markdown.tsx +221 -0
  45. package/plugins/paseo/client/square-message.tsx +64 -94
  46. package/plugins/paseo/client/transform.ts +4 -1
  47. package/plugins/paseo/shared/message.ts +6 -4
  48. package/plugins/pi/square-pi.js +26 -16
  49. package/skills/square/SKILL.md +1 -1
@@ -0,0 +1,221 @@
1
+ import { useMemo } from "react";
2
+ import { Text, View, type TextStyle } from "react-native";
3
+ import type { PluginTheme } from "@getpaseo/plugin";
4
+
5
+ interface InlinePart { text: string; bold?: boolean; italic?: boolean; code?: boolean }
6
+
7
+ // Minimal inline grammar: **bold**, *italic*, `code`. The first marker wins;
8
+ // no nesting, so action lines like *leans over* survive intact.
9
+ const INLINE_RE = /(\*\*[^*\n]+\*\*|\*[^*\n]+\*|`[^`\n]+`)/g;
10
+
11
+ export function parseInline(line: string): InlinePart[] {
12
+ const parts: InlinePart[] = [];
13
+ let last = 0;
14
+ for (const match of line.matchAll(INLINE_RE)) {
15
+ const index = match.index ?? 0;
16
+ if (index > last) parts.push({ text: line.slice(last, index) });
17
+ const token = match[0];
18
+ if (token.startsWith("**")) parts.push({ text: token.slice(2, -2), bold: true });
19
+ else if (token.startsWith("`")) parts.push({ text: token.slice(1, -1), code: true });
20
+ else parts.push({ text: token.slice(1, -1), italic: true });
21
+ last = index + token.length;
22
+ }
23
+ if (last < line.length) parts.push({ text: line.slice(last) });
24
+ return parts;
25
+ }
26
+
27
+ type Block =
28
+ | { kind: "code"; text: string }
29
+ | { kind: "heading"; level: number; text: string }
30
+ | { kind: "quote"; lines: string[] }
31
+ | { kind: "list"; items: string[] }
32
+ | { kind: "paragraph"; text: string };
33
+
34
+ function parseBlocks(markdown: string): Block[] {
35
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
36
+ const blocks: Block[] = [];
37
+ let paragraph: string[] = [];
38
+ const flushParagraph = () => {
39
+ if (paragraph.length > 0) blocks.push({ kind: "paragraph", text: paragraph.join(" ") });
40
+ paragraph = [];
41
+ };
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const line = lines[i]!;
44
+ if (line.trimStart().startsWith("```")) {
45
+ flushParagraph();
46
+ const body: string[] = [];
47
+ i += 1;
48
+ while (i < lines.length && !lines[i]!.trimStart().startsWith("```")) { body.push(lines[i]!); i += 1; }
49
+ blocks.push({ kind: "code", text: body.join("\n") });
50
+ continue;
51
+ }
52
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
53
+ if (heading) {
54
+ flushParagraph();
55
+ blocks.push({ kind: "heading", level: heading[1]!.length, text: heading[2]! });
56
+ continue;
57
+ }
58
+ if (line.startsWith(">")) {
59
+ flushParagraph();
60
+ const quote: string[] = [];
61
+ while (i < lines.length && lines[i]!.startsWith(">")) { quote.push(lines[i]!.replace(/^>\s?/, "")); i += 1; }
62
+ i -= 1;
63
+ blocks.push({ kind: "quote", lines: quote });
64
+ continue;
65
+ }
66
+ if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
67
+ flushParagraph();
68
+ const items: string[] = [];
69
+ while (i < lines.length && /^\s*(?:[-*+]|\d+\.)\s+/.test(lines[i]!)) {
70
+ const item = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/.exec(lines[i]!);
71
+ items.push(item?.[1] ?? lines[i]!);
72
+ i += 1;
73
+ }
74
+ i -= 1;
75
+ blocks.push({ kind: "list", items });
76
+ continue;
77
+ }
78
+ if (line.trim() === "") { flushParagraph(); continue; }
79
+ paragraph.push(line.trim());
80
+ }
81
+ flushParagraph();
82
+ return blocks;
83
+ }
84
+
85
+ interface SquareBlock { meta?: string; actor?: string; body: string }
86
+
87
+ // A square notification is one or more <square-activity> blocks. Split them out so the
88
+ // envelope lines become styled chrome and only the body goes through markdown.
89
+ function parseSquareBlocks(text: string): SquareBlock[] {
90
+ const blocks: SquareBlock[] = [];
91
+ const re = /<square-activity>\n([\s\S]*?)\n<\/square-activity>/g;
92
+ let last = 0;
93
+ for (const match of text.matchAll(re)) {
94
+ const index = match.index ?? 0;
95
+ const before = text.slice(last, index).trim();
96
+ if (before !== "") blocks.push({ body: before });
97
+ const inner = match[1]!;
98
+ const innerLines = inner.split("\n");
99
+ const meta = innerLines[0]?.startsWith("· ") ? innerLines.shift() : undefined;
100
+ const actor = innerLines[0]?.startsWith("● ") ? innerLines.shift() : undefined;
101
+ blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
102
+ last = index + match[0].length;
103
+ }
104
+ const rest = text.slice(last).trim();
105
+ if (rest !== "") blocks.push({ body: rest });
106
+ if (blocks.length === 0) blocks.push({ body: text });
107
+ return blocks;
108
+ }
109
+
110
+ export function SquareMarkdown({ text, theme }: { text: string; theme: PluginTheme }) {
111
+ const { colors } = theme;
112
+ const styles = useMemo(() => ({
113
+ block: { gap: 6 },
114
+ paragraph: { color: colors.foreground, fontSize: 13, lineHeight: 20 },
115
+ bold: { fontWeight: "700" as const },
116
+ italic: { fontStyle: "italic" as const },
117
+ inlineCode: {
118
+ fontFamily: "Menlo",
119
+ fontSize: 12,
120
+ backgroundColor: colors.surface2,
121
+ color: colors.foreground,
122
+ },
123
+ codeBlock: {
124
+ backgroundColor: colors.surface0,
125
+ borderRadius: 8,
126
+ borderWidth: 1,
127
+ borderColor: colors.border,
128
+ paddingHorizontal: 9,
129
+ paddingVertical: 7,
130
+ },
131
+ codeText: { fontFamily: "Menlo", fontSize: 11.5, lineHeight: 17, color: colors.foreground },
132
+ heading: { color: colors.foreground, fontWeight: "700" as const },
133
+ quote: {
134
+ borderLeftWidth: 2,
135
+ borderLeftColor: colors.border,
136
+ paddingLeft: 8,
137
+ gap: 2,
138
+ },
139
+ quoteText: { color: colors.foregroundMuted, fontSize: 13, lineHeight: 20, fontStyle: "italic" as const },
140
+ meta: { color: colors.foregroundMuted, fontSize: 11, lineHeight: 15, fontFamily: "Menlo" },
141
+ actor: { color: colors.accent, fontSize: 12, lineHeight: 17 },
142
+ separator: { height: 1, backgroundColor: colors.border, marginVertical: 4 },
143
+ listRow: { flexDirection: "row" as const, gap: 6 },
144
+ bullet: { color: colors.accent, fontSize: 13, lineHeight: 20 },
145
+ }), [colors]);
146
+
147
+ const renderInline = (line: string, keyPrefix: string, extra?: TextStyle) => (
148
+ <Text key={keyPrefix} style={[styles.paragraph, extra]}>
149
+ {parseInline(line).map((part, index) => (
150
+ <Text
151
+ key={`${keyPrefix}-${index}`}
152
+ style={[
153
+ part.bold ? styles.bold : undefined,
154
+ part.italic ? styles.italic : undefined,
155
+ part.code ? styles.inlineCode : undefined,
156
+ ]}
157
+ >{part.code ? ` ${part.text} ` : part.text}</Text>
158
+ ))}
159
+ </Text>
160
+ );
161
+
162
+ const renderBlock = (block: Block, key: string) => {
163
+ switch (block.kind) {
164
+ case "code":
165
+ return (
166
+ <View key={key} style={styles.codeBlock}>
167
+ <Text selectable style={styles.codeText}>{block.text}</Text>
168
+ </View>
169
+ );
170
+ case "heading":
171
+ return renderInline(block.text, key, { fontSize: Math.max(13, 18 - block.level), fontWeight: "700" });
172
+ case "quote":
173
+ return (
174
+ <View key={key} style={styles.quote}>
175
+ {block.lines.map((line, index) => (
176
+ <Text key={`${key}-${index}`} style={styles.quoteText}>{line}</Text>
177
+ ))}
178
+ </View>
179
+ );
180
+ case "list":
181
+ return (
182
+ <View key={key} style={{ gap: 2 }}>
183
+ {block.items.map((item, index) => (
184
+ <View key={`${key}-${index}`} style={styles.listRow}>
185
+ <Text style={styles.bullet}>•</Text>
186
+ <Text style={[styles.paragraph, { flexShrink: 1 }]}>
187
+ {parseInline(item).map((part, partIndex) => (
188
+ <Text
189
+ key={partIndex}
190
+ style={[
191
+ part.bold ? styles.bold : undefined,
192
+ part.italic ? styles.italic : undefined,
193
+ part.code ? styles.inlineCode : undefined,
194
+ ]}
195
+ >{part.code ? ` ${part.text} ` : part.text}</Text>
196
+ ))}
197
+ </Text>
198
+ </View>
199
+ ))}
200
+ </View>
201
+ );
202
+ case "paragraph":
203
+ return renderInline(block.text, key);
204
+ }
205
+ };
206
+
207
+ const blocks = useMemo(() => parseSquareBlocks(text), [text]);
208
+
209
+ return (
210
+ <View style={styles.block}>
211
+ {blocks.map((block, blockIndex) => (
212
+ <View key={blockIndex} style={styles.block}>
213
+ {blockIndex > 0 && <View style={styles.separator} />}
214
+ {block.meta !== undefined && <Text style={styles.meta}>{block.meta}</Text>}
215
+ {block.actor !== undefined && <Text style={styles.actor}>{block.actor}</Text>}
216
+ {parseBlocks(block.body).map((inner, innerIndex) => renderBlock(inner, `${blockIndex}-${innerIndex}`))}
217
+ </View>
218
+ ))}
219
+ </View>
220
+ );
221
+ }
@@ -1,117 +1,87 @@
1
1
  import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
- import { copyText, Icon, useToast } from "@getpaseo/plugin/client/react-native";
2
+ import { Icon } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMemo, useState } from "react";
4
- import { Platform, Pressable, ScrollView, Text, View } from "react-native";
4
+ import { Pressable, ScrollView, Text, View } from "react-native";
5
5
  import { squareHeadline, type SquareMessageData } from "../shared/message";
6
+ import { parseInline, SquareMarkdown } from "./markdown";
6
7
 
7
- function withOpacity(color: string, opacity: number): string {
8
- const hex = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(color);
9
- if (hex === null) return color;
10
- const value = hex[1]!.length === 3 ? [...hex[1]!].map((part) => part + part).join("") : hex[1]!;
11
- const alpha = Math.round(opacity * 255).toString(16).padStart(2, "0");
12
- return `#${value}${alpha}`;
13
- }
14
-
8
+ /** A plain card: who, what they did, where, then what they said. No badge, no chrome — the words carry the place. */
15
9
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
16
10
  const [expanded, setExpanded] = useState(false);
17
- const toast = useToast();
18
- const { text, items } = item.data;
11
+ const { text, fullText, items } = item.data;
19
12
  const latest = items?.[items.length - 1];
13
+ // Preview typography only; expansion prefers the complete renderer-only text.
20
14
  const preview = latest?.preview?.replace(/\s+/g, " ").trim();
21
15
  const muted = theme.colors.foregroundMuted;
22
16
  const foreground = theme.colors.foreground;
17
+
23
18
  const styles = useMemo(() => ({
24
- marker: { color: theme.colors.accent, fontSize: 10, marginTop: 2 },
25
- headline: { color: foreground, fontSize: 13, lineHeight: 19, flexShrink: 1 },
26
- meta: { color: muted, fontSize: 11, lineHeight: 16, flexShrink: 1 },
27
- preview: { color: foreground, fontSize: 12, lineHeight: 18, flexShrink: 1 },
28
- bodyPanel: {
19
+ card: {
20
+ marginVertical: 3,
21
+ marginRight: layout.compact ? 0 : 8,
29
22
  backgroundColor: theme.colors.surface1,
30
- borderRadius: 12,
31
- marginTop: 5,
32
- marginLeft: 17,
33
- overflow: "hidden" as const,
23
+ borderWidth: 1,
24
+ borderColor: theme.colors.border,
25
+ borderRadius: 6,
26
+ paddingHorizontal: 12,
27
+ paddingVertical: 8,
28
+ gap: 4,
34
29
  },
35
- bodyText: {
36
- color: foreground,
37
- fontSize: 12,
38
- lineHeight: 21,
39
- fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace",
40
- paddingHorizontal: layout.compact ? 10 : 12,
41
- paddingTop: 8,
42
- },
43
- }), [foreground, layout.compact, muted, theme.colors.accent, theme.colors.surface1]);
30
+ whoRow: { flexDirection: "row" as const, alignItems: "baseline" as const, gap: 6 },
31
+ glyph: { color: theme.colors.accent, fontSize: 9 },
32
+ actor: { color: foreground, fontSize: 12.5, fontWeight: "600" as const },
33
+ route: { color: muted, fontSize: 12.5, flexShrink: 1 },
34
+ count: { color: muted, fontSize: 11 },
35
+ preview: { color: foreground, fontSize: 13, lineHeight: 19 },
36
+ action: { color: muted, fontStyle: "italic" as const },
37
+ bold: { fontWeight: "700" as const },
38
+ code: { fontFamily: "Menlo", fontSize: 12 },
39
+ }), [foreground, layout.compact, muted, theme.colors.accent, theme.colors.border, theme.colors.surface1]);
40
+
41
+ const headline = latest !== undefined ? squareHeadline(latest) : "Square context";
42
+ const routeText = latest?.actor !== undefined && headline.startsWith(latest.actor)
43
+ ? headline.slice(latest.actor.length).trim()
44
+ : headline;
44
45
 
45
46
  return (
46
- <View>
47
- <View>
48
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 7, paddingVertical: 2 }}>
49
- <Text style={styles.marker}>■</Text>
50
- <Text numberOfLines={1} style={styles.headline}>
51
- {latest?.square ?? "Square context"}
47
+ <View style={styles.card}>
48
+ <Pressable
49
+ accessibilityRole="button"
50
+ accessibilityLabel={`${expanded ? "收起" : "展开"} ${latest?.square ?? "square"}: ${headline}`}
51
+ aria-expanded={expanded}
52
+ onPress={() => setExpanded(value => !value)}
53
+ style={({ pressed }) => ({ gap: 4, opacity: pressed ? 0.7 : 1 })}
54
+ >
55
+ <View style={styles.whoRow}>
56
+ <Text style={styles.glyph}>■</Text>
57
+ {latest?.actor !== undefined && <Text style={styles.actor}>{latest.actor}</Text>}
58
+ <Text numberOfLines={1} style={styles.route}>
59
+ {routeText}{latest?.square !== undefined && ` · ${latest.square}`}
52
60
  </Text>
53
- {items && items.length > 1 && <Text style={styles.meta}>· {items.length} landed</Text>}
54
61
  <View style={{ flex: 1 }} />
55
- {!expanded && <Pressable
56
- accessibilityRole="button"
57
- accessibilityLabel={`展开 ${latest !== undefined ? squareHeadline(latest) : "Square context"}`}
58
- aria-expanded={false}
59
- onPress={() => setExpanded(true)}
60
- hitSlop={6}
61
- style={({ pressed }) => ({ padding: 5, opacity: pressed ? 0.6 : 1 })}
62
- >
63
- <Icon name="ChevronDown" size={18} color={muted} />
64
- </Pressable>}
62
+ {items !== undefined && items.length > 1 && <Text style={styles.count}>{items.length}</Text>}
63
+ <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={16} color={muted} />
65
64
  </View>
66
- {(latest?.actor !== undefined || preview) && <View style={{ marginLeft: 17, marginTop: 2 }}>
67
- <Text numberOfLines={1} style={styles.preview}>
68
- <Text style={{ color: theme.colors.accent, fontSize: 10 }}>● </Text>
69
- {latest?.actor !== undefined && <Text style={styles.meta}>{latest.actor} </Text>}
70
- {preview}
65
+ {!expanded && preview !== undefined && preview !== "" && (
66
+ <Text numberOfLines={2} style={styles.preview}>
67
+ {parseInline(preview).map((part, index) => (
68
+ <Text
69
+ key={index}
70
+ style={[
71
+ part.italic ? styles.action : undefined,
72
+ part.bold ? styles.bold : undefined,
73
+ part.code ? styles.code : undefined,
74
+ ]}
75
+ >{part.text}</Text>
76
+ ))}
71
77
  </Text>
72
- </View>}
73
- </View>
74
- {expanded && <View style={styles.bodyPanel}>
75
- <ScrollView style={{ maxHeight: layout.compact ? 280 : 400 }} nestedScrollEnabled>
76
- <Text selectable style={styles.bodyText}>{text}</Text>
78
+ )}
79
+ </Pressable>
80
+ {expanded && (
81
+ <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 2 }} nestedScrollEnabled>
82
+ <SquareMarkdown text={fullText ?? text} theme={theme} />
77
83
  </ScrollView>
78
- <View style={{
79
- marginHorizontal: layout.compact ? 10 : 12,
80
- marginTop: 5,
81
- borderTopWidth: 1,
82
- borderTopColor: withOpacity(theme.colors.border, 0.55),
83
- paddingVertical: 3,
84
- flexDirection: "row",
85
- alignItems: "center",
86
- justifyContent: "space-between",
87
- }}>
88
- <Pressable
89
- accessibilityRole="button"
90
- accessibilityLabel="Copy Square content"
91
- onPress={() => { void copyText(text).catch(() => toast.error("Could not copy Square content")); }}
92
- style={({ pressed }) => ({
93
- flexDirection: "row",
94
- alignItems: "center",
95
- gap: 5,
96
- paddingHorizontal: 2,
97
- opacity: pressed ? 0.6 : 1,
98
- })}
99
- >
100
- <Icon name="Copy" size={12} color={muted} />
101
- <Text style={{ color: muted, fontSize: 11 }}>Copy content</Text>
102
- </Pressable>
103
- <Pressable
104
- accessibilityRole="button"
105
- accessibilityLabel="收起 Square context"
106
- aria-expanded={true}
107
- onPress={() => setExpanded(false)}
108
- hitSlop={6}
109
- style={({ pressed }) => ({ paddingHorizontal: 5, paddingVertical: 1, opacity: pressed ? 0.6 : 1 })}
110
- >
111
- <Icon name="ChevronUp" size={18} color={muted} />
112
- </Pressable>
113
- </View>
114
- </View>}
84
+ )}
115
85
  </View>
116
86
  );
117
87
  }
@@ -17,7 +17,10 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
17
17
  version: 1,
18
18
  data: {
19
19
  text: item.detail.text,
20
- ...(details.success ? { items: details.data.items } : {}),
20
+ ...(details.success ? {
21
+ items: details.data.items,
22
+ ...(details.data.fullText === undefined ? {} : { fullText: details.data.fullText }),
23
+ } : {}),
21
24
  },
22
25
  }],
23
26
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  export const squareCardDetailsSchema = z.object({
4
+ fullText: z.string().optional(),
4
5
  items: z.array(z.object({
5
6
  square: z.string(),
6
7
  actor: z.string().optional(),
@@ -16,15 +17,16 @@ export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["i
16
17
  if (item.actor === undefined || item.route === undefined) return item.description;
17
18
  const to = item.recipient;
18
19
  switch (item.route) {
19
- case "mention": return to === undefined ? `${item.actor} mentioned you` : `${item.actor} mentioned ${to}`;
20
- case "reply": return to === undefined ? `${item.actor} replied to you` : `${item.actor} replied to ${to}`;
21
- case "attention": return `New from ${item.actor}`;
22
- case "bell": return `${item.actor} called everyone`;
20
+ case "mention": return to === undefined ? `${item.actor} spoke your name` : `${item.actor} spoke ${to}'s name`;
21
+ case "reply": return to === undefined ? `${item.actor} answered you` : `${item.actor} answered ${to}`;
22
+ case "attention": return to === undefined ? `${item.actor} spoke · you're listening` : `${item.actor} spoke · ${to} is listening`;
23
+ case "bell": return `${item.actor} rang the bell`;
23
24
  }
24
25
  }
25
26
 
26
27
  export const squareMessageSchema = z.object({
27
28
  text: z.string(),
29
+ fullText: z.string().optional(),
28
30
  items: squareCardDetailsSchema.shape.items.optional(),
29
31
  });
30
32
  export type SquareMessageData = z.output<typeof squareMessageSchema>;
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
+ import { Text } from '@earendil-works/pi-tui';
2
3
 
3
4
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
4
5
  import { automaticSessionEnd, automaticSessionStart } from '../../dist/automatic-session.js';
5
6
  import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
6
- import { previewAttentionBody, renderAttentionDescription } from '../../dist/attention-presentation.js';
7
+ import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
7
8
  import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
8
9
 
9
10
  class PiDeliveryDroppedError extends Error {
@@ -31,7 +32,7 @@ function framePiMessage(content) {
31
32
  return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
32
33
  }
33
34
 
34
- /** Renderer-only facts for the collapsed card; Pi never sends `details` to the model. */
35
+ /** Renderer-only summary and full text; Pi never sends `details` to the model. */
35
36
  export function pendingCardDetails(pending) {
36
37
  const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
37
38
  square: path.basename(membership.squarePath, '.square'),
@@ -41,7 +42,14 @@ export function pendingCardDetails(pending) {
41
42
  preview: previewAttentionBody(note.body),
42
43
  description: renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route }),
43
44
  })));
44
- return items.length === 0 ? undefined : { items };
45
+ if (items.length === 0) return undefined;
46
+ const fullText = pending.flatMap((membership) => membership.notifications.map((note) => renderAttentionFull({
47
+ squarePath: membership.squarePath,
48
+ actIndex: note.actIndex,
49
+ recipient: membership.name,
50
+ ...note,
51
+ }))).join('\n\n');
52
+ return { items, fullText };
45
53
  }
46
54
 
47
55
  // Conservative terminal width: anything outside ASCII counts as two columns, so a
@@ -62,12 +70,17 @@ function fitColumns(text, columns) {
62
70
  }
63
71
 
64
72
  /**
65
- * Collapsed: one line naming who reached you and where. Expanded: Pi's default card,
66
- * which shows the exact content the model received.
73
+ * Collapsed: one line naming who reached you and where. Expanded: the complete
74
+ * notification text from renderer details, wrapped by Pi's terminal component.
67
75
  */
68
76
  export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
77
+ if (expanded) {
78
+ return typeof message.details?.fullText === 'string'
79
+ ? new Text(message.details.fullText, outputPad, 0)
80
+ : undefined;
81
+ }
69
82
  const items = message.details?.items;
70
- if (expanded || !Array.isArray(items) || items.length === 0) return undefined;
83
+ if (!Array.isArray(items) || items.length === 0) return undefined;
71
84
  const [first] = items;
72
85
  const tail = ` · ${first.square}${items.length > 1 ? ` +${items.length - 1}` : ''}`;
73
86
  return {
@@ -240,8 +253,8 @@ export default function squarePiExtension(pi) {
240
253
  boundSessionId,
241
254
  async (content) => {
242
255
  if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
243
- // One card for the human: collapsed it names who reached you, expanded it
244
- // shows the same content the model receives.
256
+ // One card for the human: a compact heading, expandable to the full body.
257
+ // Model context stays bounded; renderer-only details carry the complete text.
245
258
  const messageContent = framePiMessage(content);
246
259
  const landing = waitForLanding(messageContent, signal, 'steer');
247
260
  try {
@@ -254,16 +267,13 @@ export default function squarePiExtension(pi) {
254
267
  }
255
268
  return landing.promise;
256
269
  },
257
- async (id, env) => {
258
- const inbox = await sessionInbox(id, env ?? sessionEnv(id));
270
+ async (_id, _env, boundarySignal) => {
271
+ // Recheck committed revisions and current bindings before reusing observed content.
272
+ const inbox = await observer.read({ signal: boundarySignal, excludeKeys: handledPending });
259
273
  if (signal.aborted || token !== generation) return [];
260
274
  for (const key of inboxKeys(inbox)) observedPending.add(key);
261
- return inbox.map((membership) => ({
262
- ...membership,
263
- notifications: membership.notifications.filter((note) => !handledPending.has(
264
- `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${note.actIndex}`,
265
- )),
266
- }));
275
+ pending = inbox;
276
+ return inbox;
267
277
  },
268
278
  env,
269
279
  signal,
@@ -65,7 +65,7 @@ The ownership boundary belongs here. Does this match your read?
65
65
  EOF
66
66
  ```
67
67
 
68
- **Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off, and an unmatched `@name` is rejected rather than landing bare. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
68
+ **Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
69
69
 
70
70
  Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
71
71