@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.
- package/dist/attention-presentation.d.ts +2 -0
- package/dist/attention-presentation.js +11 -2
- package/dist/boundary-presentation.d.ts +8 -1
- package/dist/boundary-presentation.js +8 -1
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/express-presentation.js +1 -2
- package/dist/cli/ledger-command.js +6 -2
- package/dist/cli/meta-commands.js +1 -1
- package/dist/decisions.js +6 -21
- package/dist/help.js +4 -4
- package/dist/host-ledger-migration.d.ts +1 -0
- package/dist/host-ledger-migration.js +62 -21
- package/dist/host-ledger-sqlite.d.ts +4 -1
- package/dist/host-ledger-sqlite.js +36 -9
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +64 -22
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +51 -26
- package/dist/model.d.ts +2 -3
- package/dist/model.js +5 -2
- package/dist/open-square.d.ts +12 -2
- package/dist/open-square.js +42 -4
- package/dist/participant-host.js +6 -2
- package/dist/ports.d.ts +10 -0
- package/dist/presentation.js +1 -1
- package/dist/square-actions.d.ts +3 -1
- package/dist/square-actions.js +10 -5
- package/dist/square-application.d.ts +4 -0
- package/dist/square-application.js +42 -28
- package/dist/square-facade.d.ts +26 -22
- package/dist/views.d.ts +3 -1
- package/dist/views.js +37 -29
- package/guides/application-demand-optimization.md +50 -0
- package/guides/catch-cold-path-optimization.md +56 -0
- package/guides/catch-lock-investigation.md +81 -0
- package/guides/host-ledger.md +2 -2
- package/guides/mcp-connection-lifetime.md +22 -0
- package/guides/pi-boundary-pending-reuse.md +13 -0
- package/package.json +2 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +10 -5
- package/plugins/paseo/client/markdown.tsx +221 -0
- package/plugins/paseo/client/square-message.tsx +64 -94
- package/plugins/paseo/client/transform.ts +4 -1
- package/plugins/paseo/shared/message.ts +6 -4
- package/plugins/pi/square-pi.js +26 -16
- 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 {
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.colors.border,
|
|
25
|
+
borderRadius: 6,
|
|
26
|
+
paddingHorizontal: 12,
|
|
27
|
+
paddingVertical: 8,
|
|
28
|
+
gap: 4,
|
|
34
29
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
|
|
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
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
{
|
|
56
|
-
|
|
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
|
-
{
|
|
67
|
-
<Text numberOfLines={
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
</
|
|
74
|
-
{expanded &&
|
|
75
|
-
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400 }} nestedScrollEnabled>
|
|
76
|
-
<
|
|
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
|
-
|
|
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 ? {
|
|
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}
|
|
20
|
-
case "reply": return to === undefined ? `${item.actor}
|
|
21
|
-
case "attention": return `
|
|
22
|
-
case "bell": return `${item.actor}
|
|
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>;
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
66
|
-
*
|
|
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 (
|
|
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:
|
|
244
|
-
//
|
|
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 (
|
|
258
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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,
|
package/skills/square/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|