@astrosheep/square 0.3.88 → 0.3.89

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 (51) hide show
  1. package/dist/artifact.js +11 -9
  2. package/dist/boundary-presentation.d.ts +3 -2
  3. package/dist/boundary-presentation.js +50 -77
  4. package/dist/cli/context.js +1 -3
  5. package/dist/delivery-deferral.d.ts +28 -0
  6. package/dist/delivery-deferral.js +83 -0
  7. package/dist/delivery-operations.js +8 -6
  8. package/dist/delivery.js +4 -1
  9. package/dist/inbox.js +21 -10
  10. package/dist/mcp.js +0 -3
  11. package/dist/model.d.ts +1 -0
  12. package/dist/operation-presentation.js +0 -4
  13. package/dist/ports.d.ts +11 -3
  14. package/dist/presentation-operations.d.ts +20 -1
  15. package/dist/presentation-operations.js +68 -44
  16. package/dist/sqlite-square.js +35 -13
  17. package/dist/square-domain.d.ts +1 -1
  18. package/dist/square-domain.js +12 -0
  19. package/dist/square-facade.d.ts +8 -2
  20. package/dist/square-migration.d.ts +2 -1
  21. package/dist/square-migration.js +25 -26
  22. package/dist/square-sqlite.d.ts +5 -2
  23. package/dist/square-sqlite.js +79 -6
  24. package/dist/square-stdio.d.ts +1 -1
  25. package/dist/square-stdio.js +30 -17
  26. package/dist/square-tools.d.ts +1 -1
  27. package/dist/square-tools.js +25 -51
  28. package/dist/stdio-server.js +23 -3
  29. package/dist/views.js +0 -2
  30. package/guides/backends.md +26 -13
  31. package/guides/change-waits.md +40 -11
  32. package/guides/claude-native.md +9 -9
  33. package/guides/mcp.md +1 -1
  34. package/guides/migration.md +30 -12
  35. package/guides/operation-cancellation.md +16 -1
  36. package/guides/participant-ownership.md +60 -186
  37. package/guides/pi-tools.md +1 -1
  38. package/guides/square-api.md +72 -9
  39. package/package.json +1 -1
  40. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  41. package/plugins/claude/skills/square/SKILL.md +3 -3
  42. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  43. package/plugins/paseo/README.md +7 -6
  44. package/plugins/paseo/client/markdown.tsx +125 -185
  45. package/plugins/paseo/client/square-message.tsx +9 -25
  46. package/plugins/paseo/client/transform.ts +5 -4
  47. package/plugins/paseo/index.client.tsx +14 -12
  48. package/plugins/paseo/package.json +3 -0
  49. package/plugins/paseo/shared/message.ts +50 -1
  50. package/plugins/pi/square-pi.js +1 -1
  51. package/skills/square/SKILL.md +3 -3
@@ -1,135 +1,22 @@
1
- import { useMemo } from "react";
2
- import { Text, View, type TextStyle } from "react-native";
1
+ import { useMemo, type ReactNode } from "react";
2
+ import { Platform, Text, View, type TextStyle } from "react-native";
3
3
  import type { PluginTheme } from "@getpaseo/plugin";
4
+ import { Lexer, lexer, type Token, type Tokens } from "marked";
5
+ import { parseSquareBlocks } from "../shared/message.ts";
4
6
 
5
- interface InlinePart { text: string; bold?: boolean; italic?: boolean; code?: boolean }
7
+ export { parseSquareBlocks };
6
8
 
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;
9
+ export const MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "ui-monospace, Menlo, monospace" });
10
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
- function parseAttributes(source: string): Record<string, string> {
88
- const attributes: Record<string, string> = {};
89
- for (const match of source.matchAll(/([a-z]+)="([^"]*)"/g)) {
90
- attributes[match[1]!] = match[2]!.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
91
- }
92
- return attributes;
93
- }
94
-
95
- // A square notification is one or more <square-activity> blocks. Split them out so the
96
- // envelope becomes styled chrome and only the body goes through markdown: where and which
97
- // activity come from tag attributes, and the first inner line is the route sentence.
98
- function parseSquareBlocks(text: string): SquareBlock[] {
99
- const blocks: SquareBlock[] = [];
100
- const re = /<square-activity([^>]*)>\n([\s\S]*?)\n<\/square-activity>/g;
101
- let last = 0;
102
- for (const match of text.matchAll(re)) {
103
- const index = match.index ?? 0;
104
- const before = text.slice(last, index).trim();
105
- if (before !== "") blocks.push({ body: before });
106
- const attributes = parseAttributes(match[1]!);
107
- const innerLines = match[2]!.split("\n");
108
- const described = [attributes.square, attributes.as, attributes.id].filter((value) => value !== undefined);
109
- const meta = described.length > 0 ? `· ${described.join(" · ")}` : undefined;
110
- const actor = innerLines.shift();
111
- blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
112
- last = index + match[0].length;
113
- }
114
- const rest = text.slice(last).trim();
115
- if (rest !== "") blocks.push({ body: rest });
116
- if (blocks.length === 0) blocks.push({ body: text });
117
- return blocks;
118
- }
119
-
120
- export function SquareMarkdown({ text, theme }: { text: string; theme: PluginTheme }) {
121
- const { colors } = theme;
122
- const styles = useMemo(() => ({
11
+ function makeStyles(colors: PluginTheme["colors"]) {
12
+ return {
123
13
  block: { gap: 6 },
124
- paragraph: { color: colors.foreground, fontSize: 13, lineHeight: 20 },
125
- bold: { fontWeight: "700" as const },
126
- italic: { fontStyle: "italic" as const },
127
- inlineCode: {
128
- fontFamily: "Menlo",
129
- fontSize: 12,
130
- backgroundColor: colors.surface2,
131
- color: colors.foreground,
132
- },
14
+ text: { color: colors.foreground, fontSize: 13, lineHeight: 19 },
15
+ strong: { fontWeight: "700" as const },
16
+ em: { fontStyle: "italic" as const },
17
+ del: { textDecorationLine: "line-through" as const },
18
+ link: { color: colors.accent, textDecorationLine: "underline" as const },
19
+ inlineCode: { fontFamily: MONO, fontSize: 12, backgroundColor: colors.surface2, color: colors.foreground },
133
20
  codeBlock: {
134
21
  backgroundColor: colors.surface0,
135
22
  borderRadius: 8,
@@ -138,92 +25,145 @@ export function SquareMarkdown({ text, theme }: { text: string; theme: PluginThe
138
25
  paddingHorizontal: 9,
139
26
  paddingVertical: 7,
140
27
  },
141
- codeText: { fontFamily: "Menlo", fontSize: 11.5, lineHeight: 17, color: colors.foreground },
142
- heading: { color: colors.foreground, fontWeight: "700" as const },
143
- quote: {
144
- borderLeftWidth: 2,
145
- borderLeftColor: colors.border,
146
- paddingLeft: 8,
147
- gap: 2,
148
- },
149
- quoteText: { color: colors.foregroundMuted, fontSize: 13, lineHeight: 20, fontStyle: "italic" as const },
150
- meta: { color: colors.foregroundMuted, fontSize: 11, lineHeight: 15, fontFamily: "Menlo" },
28
+ codeText: { fontFamily: MONO, fontSize: 11.5, lineHeight: 17, color: colors.foreground },
29
+ quote: { borderLeftWidth: 2, borderLeftColor: colors.border, paddingLeft: 8, gap: 2 },
30
+ quoteText: { color: colors.foregroundMuted, fontStyle: "italic" as const },
31
+ meta: { color: colors.foregroundMuted, fontSize: 11, lineHeight: 15, fontFamily: MONO },
151
32
  actor: { color: colors.accent, fontSize: 12, lineHeight: 17 },
152
33
  separator: { height: 1, backgroundColor: colors.border, marginVertical: 4 },
153
34
  listRow: { flexDirection: "row" as const, gap: 6 },
154
- bullet: { color: colors.accent, fontSize: 13, lineHeight: 20 },
155
- }), [colors]);
35
+ listItem: { flexShrink: 1, gap: 2 },
36
+ bullet: { color: colors.accent, fontSize: 13, lineHeight: 19 },
37
+ tableRow: { flexDirection: "row" as const, gap: 8 },
38
+ tableCell: { flex: 1, fontSize: 12 },
39
+ };
40
+ }
156
41
 
157
- const renderInline = (line: string, keyPrefix: string, extra?: TextStyle) => (
158
- <Text key={keyPrefix} style={[styles.paragraph, extra]}>
159
- {parseInline(line).map((part, index) => (
160
- <Text
161
- key={`${keyPrefix}-${index}`}
162
- style={[
163
- part.bold ? styles.bold : undefined,
164
- part.italic ? styles.italic : undefined,
165
- part.code ? styles.inlineCode : undefined,
166
- ]}
167
- >{part.code ? ` ${part.text} ` : part.text}</Text>
168
- ))}
169
- </Text>
170
- );
42
+ type Styles = ReturnType<typeof makeStyles>;
43
+
44
+ /** Children of a token, if it has any: `Token` is a union, so the optional field needs one cast. */
45
+ const children = (node: Token | { tokens?: Token[] }): Token[] => (node as { tokens?: Token[] }).tokens ?? [];
171
46
 
172
- const renderBlock = (block: Block, key: string) => {
173
- switch (block.kind) {
47
+ function renderInlines(tokens: Token[], styles: Styles, prefix: string, extra?: TextStyle): ReactNode[] {
48
+ return tokens.map((token, index) => {
49
+ const key = `${prefix}-${index}`;
50
+ const nested = (style: TextStyle): ReactNode => (
51
+ <Text key={key} style={[styles.text, extra, style]}>{renderInlines(children(token), styles, key)}</Text>
52
+ );
53
+ switch (token.type) {
54
+ case "strong": return nested(styles.strong);
55
+ case "em": return nested(styles.em);
56
+ case "del": return nested(styles.del);
57
+ case "link": return nested(styles.link);
58
+ case "codespan": return <Text key={key} style={[styles.text, extra, styles.inlineCode]}>{` ${token.text} `}</Text>;
59
+ case "br": return "\n";
60
+ case "image": return token.text;
61
+ default: {
62
+ const inline = token as Tokens.Generic;
63
+ if (inline.tokens !== undefined) return nested({});
64
+ return inline.text ?? inline.raw;
65
+ }
66
+ }
67
+ });
68
+ }
69
+
70
+ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle?: TextStyle): ReactNode[] {
71
+ return tokens.map((token, index) => {
72
+ const key = `${prefix}-${index}`;
73
+ switch (token.type) {
74
+ case "space":
75
+ case "checkbox":
76
+ case "def":
77
+ case "html":
78
+ return null;
79
+ case "hr":
80
+ return <View key={key} style={styles.separator} />;
174
81
  case "code":
175
82
  return (
176
83
  <View key={key} style={styles.codeBlock}>
177
- <Text selectable style={styles.codeText}>{block.text}</Text>
84
+ <Text selectable style={styles.codeText}>{token.text}</Text>
178
85
  </View>
179
86
  );
180
87
  case "heading":
181
- return renderInline(block.text, key, { fontSize: Math.max(13, 18 - block.level), fontWeight: "700" });
182
- case "quote":
183
88
  return (
184
- <View key={key} style={styles.quote}>
185
- {block.lines.map((line, index) => (
186
- <Text key={`${key}-${index}`} style={styles.quoteText}>{line}</Text>
89
+ <Text key={key} style={[styles.text, textStyle, styles.strong, { fontSize: Math.max(13, 18 - token.depth) }]}>
90
+ {renderInlines(children(token), styles, key)}
91
+ </Text>
92
+ );
93
+ case "blockquote":
94
+ return <View key={key} style={styles.quote}>{renderBlocks(children(token), styles, key, styles.quoteText)}</View>;
95
+ case "list": {
96
+ const list = token as Tokens.List;
97
+ const start = typeof list.start === "number" ? list.start : 1;
98
+ return (
99
+ <View key={key} style={{ gap: 2 }}>
100
+ {list.items.map((item, itemIndex) => (
101
+ <View key={`${key}-${itemIndex}`} style={styles.listRow}>
102
+ <Text style={styles.bullet}>
103
+ {item.task ? (item.checked === true ? "☑" : "☐") : list.ordered ? `${start + itemIndex}.` : "•"}
104
+ </Text>
105
+ <View style={styles.listItem}>{renderBlocks(children(item), styles, `${key}-${itemIndex}`, textStyle)}</View>
106
+ </View>
187
107
  ))}
188
108
  </View>
189
109
  );
190
- case "list":
110
+ }
111
+ case "table": {
112
+ const table = token as Tokens.Table;
113
+ const rows = [table.header, ...table.rows];
191
114
  return (
192
115
  <View key={key} style={{ gap: 2 }}>
193
- {block.items.map((item, index) => (
194
- <View key={`${key}-${index}`} style={styles.listRow}>
195
- <Text style={styles.bullet}>•</Text>
196
- <Text style={[styles.paragraph, { flexShrink: 1 }]}>
197
- {parseInline(item).map((part, partIndex) => (
198
- <Text
199
- key={partIndex}
200
- style={[
201
- part.bold ? styles.bold : undefined,
202
- part.italic ? styles.italic : undefined,
203
- part.code ? styles.inlineCode : undefined,
204
- ]}
205
- >{part.code ? ` ${part.text} ` : part.text}</Text>
206
- ))}
207
- </Text>
116
+ {rows.map((cells, rowIndex) => (
117
+ <View key={`${key}-${rowIndex}`} style={styles.tableRow}>
118
+ {cells.map((cell, cellIndex) => (
119
+ <Text
120
+ key={`${key}-${rowIndex}-${cellIndex}`}
121
+ style={[styles.text, textStyle, styles.tableCell, rowIndex === 0 ? styles.strong : undefined]}
122
+ >{renderInlines(children(cell), styles, `${key}-${rowIndex}-${cellIndex}`)}</Text>
123
+ ))}
208
124
  </View>
209
125
  ))}
210
126
  </View>
211
127
  );
212
- case "paragraph":
213
- return renderInline(block.text, key);
128
+ }
129
+ default: {
130
+ const block = token as Tokens.Generic;
131
+ return (
132
+ <Text key={key} style={[styles.text, textStyle]}>
133
+ {renderInlines(block.tokens ?? Lexer.lexInline(block.text ?? block.raw), styles, key)}
134
+ </Text>
135
+ );
136
+ }
214
137
  }
215
- };
138
+ });
139
+ }
216
140
 
217
- const blocks = useMemo(() => parseSquareBlocks(text), [text]);
141
+ /** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
142
+ export function SquareInline({ text, theme, numberOfLines, style }: {
143
+ text: string;
144
+ theme: PluginTheme;
145
+ numberOfLines?: number;
146
+ style?: TextStyle;
147
+ }) {
148
+ const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
149
+ return (
150
+ <Text numberOfLines={numberOfLines} style={[styles.text, style]}>
151
+ {renderInlines(Lexer.lexInline(text), styles, "inline")}
152
+ </Text>
153
+ );
154
+ }
218
155
 
156
+ export function SquareMarkdown({ text, theme }: { text: string; theme: PluginTheme }) {
157
+ const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
158
+ const blocks = useMemo(() => parseSquareBlocks(text), [text]);
219
159
  return (
220
160
  <View style={styles.block}>
221
- {blocks.map((block, blockIndex) => (
222
- <View key={blockIndex} style={styles.block}>
223
- {blockIndex > 0 && <View style={styles.separator} />}
161
+ {blocks.map((block, index) => (
162
+ <View key={index} style={styles.block}>
163
+ {index > 0 && <View style={styles.separator} />}
224
164
  {block.meta !== undefined && <Text style={styles.meta}>{block.meta}</Text>}
225
165
  {block.actor !== undefined && <Text style={styles.actor}>{block.actor}</Text>}
226
- {parseBlocks(block.body).map((inner, innerIndex) => renderBlock(inner, `${blockIndex}-${innerIndex}`))}
166
+ {renderBlocks(lexer(block.body), styles, `${index}`)}
227
167
  </View>
228
168
  ))}
229
169
  </View>
@@ -2,15 +2,15 @@ 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 { squareHeadline, type SquareMessageData } from "../shared/message";
6
- import { parseInline, SquareMarkdown } from "./markdown";
5
+ import { outputPreview, squareHeadline, type SquareMessageData } from "../shared/message";
6
+ import { SquareInline, SquareMarkdown } from "./markdown";
7
7
 
8
8
  /** A plain card: who, what they did, where, then what they said. No badge, no chrome — the words carry the place. */
9
9
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
10
10
  const [expanded, setExpanded] = useState(false);
11
- const { text, fullText, items } = item.data;
11
+ const { text, items } = item.data;
12
12
  const latest = items?.[items.length - 1];
13
- // Preview typography only; expansion prefers the complete renderer-only text.
13
+ // Transform already bounded the collapsed preview; only normalise whitespace for the single line.
14
14
  const preview = latest?.preview?.replace(/\s+/g, " ").trim();
15
15
  const muted = theme.colors.foregroundMuted;
16
16
  const foreground = theme.colors.foreground;
@@ -27,15 +27,11 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
27
27
  paddingVertical: 8,
28
28
  gap: 4,
29
29
  },
30
- whoRow: { flexDirection: "row" as const, alignItems: "baseline" as const, gap: 6 },
30
+ whoRow: { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 },
31
31
  glyph: { color: theme.colors.accent, fontSize: 9 },
32
32
  actor: { color: foreground, fontSize: 12.5, fontWeight: "600" as const },
33
- route: { color: muted, fontSize: 12.5, flexShrink: 1 },
33
+ route: { color: muted, fontSize: 12.5, flex: 1 },
34
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
35
  }), [foreground, layout.compact, muted, theme.colors.accent, theme.colors.border, theme.colors.surface1]);
40
36
 
41
37
  const headline = latest !== undefined ? squareHeadline(latest) : "Square context";
@@ -58,28 +54,16 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
58
54
  <Text numberOfLines={1} style={styles.route}>
59
55
  {routeText}{latest?.square !== undefined && ` · ${latest.square}`}
60
56
  </Text>
61
- <View style={{ flex: 1 }} />
62
57
  {items !== undefined && items.length > 1 && <Text style={styles.count}>{items.length}</Text>}
63
58
  <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={16} color={muted} />
64
59
  </View>
65
60
  {!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
- ))}
77
- </Text>
61
+ <SquareInline text={preview} theme={theme} numberOfLines={2} />
78
62
  )}
79
63
  </Pressable>
80
64
  {expanded && (
81
- <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 2 }} nestedScrollEnabled>
82
- <SquareMarkdown text={fullText ?? text} theme={theme} />
65
+ <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 2, borderTopWidth: 1, borderTopColor: theme.colors.border }} contentContainerStyle={{ paddingTop: 8 }} nestedScrollEnabled>
66
+ <SquareMarkdown text={outputPreview(text)} theme={theme} />
83
67
  </ScrollView>
84
68
  )}
85
69
  </View>
@@ -1,5 +1,5 @@
1
1
  import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
2
- import { squareCardDetailsSchema } from "../shared/message.ts";
2
+ import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, squareCardDetailsSchema } from "../shared/message.ts";
3
3
 
4
4
  // Recognition is metadata-based, not a guess from a tool name or body text.
5
5
  // The same projection applies to live events and fetched history.
@@ -16,10 +16,11 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
16
16
  kind: "square-message",
17
17
  version: 1,
18
18
  data: {
19
- text: item.detail.text,
19
+ text: outputPreview(details.success && details.data.fullText !== undefined ? details.data.fullText : item.detail.text),
20
20
  ...(details.success ? {
21
- items: details.data.items,
22
- ...(details.data.fullText === undefined ? {} : { fullText: details.data.fullText }),
21
+ // Only the last item's preview is rendered, so only it takes the visible budget.
22
+ items: details.data.items.map((item, index, all) =>
23
+ index < all.length - 1 || item.preview === undefined ? item : { ...item, preview: outputPreview(item.preview, COLLAPSED_PREVIEW_MAX_CHARS) }),
23
24
  } : {}),
24
25
  },
25
26
  }],
@@ -4,16 +4,18 @@ import { transformSquareMessage } from "./client/transform";
4
4
  import { squareMessageSchema } from "./shared/message";
5
5
 
6
6
  export default function contribute(client: PluginClientContext) {
7
- client.addTimelineTransformer({
8
- id: "square-message",
9
- query: { itemType: "tool_call" },
10
- transform: transformSquareMessage,
11
- });
12
- client.addTimelineRenderer({
13
- kind: "square-message",
14
- version: 1,
15
- schema: squareMessageSchema,
16
- Component: SquareMessage,
17
- });
18
- return () => {};
7
+ const removals = [
8
+ client.addTimelineTransformer({
9
+ id: "square-message",
10
+ query: { itemType: "tool_call" },
11
+ transform: transformSquareMessage,
12
+ }),
13
+ client.addTimelineRenderer({
14
+ kind: "square-message",
15
+ version: 1,
16
+ schema: squareMessageSchema,
17
+ Component: SquareMessage,
18
+ }),
19
+ ];
20
+ return () => { for (const remove of removals) remove(); };
19
21
  }
@@ -7,6 +7,9 @@
7
7
  "typecheck": "tsc --noEmit",
8
8
  "test": "node --experimental-strip-types --test test/*.test.ts"
9
9
  },
10
+ "dependencies": {
11
+ "marked": "^17.0.6"
12
+ },
10
13
  "devDependencies": {
11
14
  "@getpaseo/plugin": "0.11.2",
12
15
  "@types/react": "~19.2.0",
@@ -1,5 +1,55 @@
1
1
  import { z } from "zod";
2
2
 
3
+ /** Client display budget, including the omission line; source history stays unchanged. */
4
+ export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
5
+ export const COLLAPSED_PREVIEW_MAX_CHARS = 1_000;
6
+ export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS): string {
7
+ if (text.length <= maxChars) return text;
8
+ const middle = "\n\n… Showing the beginning and end; the middle is hidden in this preview. …\n\n";
9
+ const headChars = Math.floor((maxChars - middle.length) / 2);
10
+ // Budget in UTF-16 units, but never split a surrogate pair.
11
+ const head = text.slice(0, headChars).replace(/[\uD800-\uDBFF]$/, "");
12
+ const tail = text.slice(-(maxChars - middle.length - headChars)).replace(/^[\uDC00-\uDFFF]/, "");
13
+ return head + middle + tail;
14
+ }
15
+
16
+ export interface SquareBlock { meta?: string; actor?: string; body: string }
17
+
18
+ function parseAttributes(source: string): Record<string, string> {
19
+ const attributes: Record<string, string> = {};
20
+ for (const match of source.matchAll(/([a-z]+)="([^"]*)"/g)) {
21
+ attributes[match[1]!] = match[2]!.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
22
+ }
23
+ return attributes;
24
+ }
25
+
26
+ // A square notification is one or more <square-activity> blocks. Split them out so the
27
+ // envelope becomes styled chrome and only the body goes through markdown: where and which
28
+ // activity come from tag attributes, or from the leading `· ...` line when the tag has none.
29
+ export function parseSquareBlocks(text: string): SquareBlock[] {
30
+ const blocks: SquareBlock[] = [];
31
+ const re = /<square-activity([^>]*)>\n([\s\S]*?)\n<\/square-activity>/g;
32
+ let last = 0;
33
+ for (const match of text.matchAll(re)) {
34
+ const index = match.index ?? 0;
35
+ const before = text.slice(last, index).trim();
36
+ if (before !== "") blocks.push({ body: before });
37
+ const attributes = parseAttributes(match[1]!);
38
+ const innerLines = match[2]!.split("\n");
39
+ const described = [attributes.square, attributes.as, attributes.id].filter((value) => value !== undefined);
40
+ const meta = described.length > 0
41
+ ? `· ${described.join(" · ")}`
42
+ : innerLines[0]?.startsWith("· ") ? innerLines.shift() : undefined;
43
+ const actor = innerLines.shift();
44
+ blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
45
+ last = index + match[0].length;
46
+ }
47
+ const rest = text.slice(last).trim();
48
+ if (rest !== "") blocks.push({ body: rest });
49
+ if (blocks.length === 0) blocks.push({ body: text });
50
+ return blocks;
51
+ }
52
+
3
53
  export const squareCardDetailsSchema = z.object({
4
54
  fullText: z.string().optional(),
5
55
  items: z.array(z.object({
@@ -26,7 +76,6 @@ export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["i
26
76
 
27
77
  export const squareMessageSchema = z.object({
28
78
  text: z.string(),
29
- fullText: z.string().optional(),
30
79
  items: squareCardDetailsSchema.shape.items.optional(),
31
80
  });
32
81
  export type SquareMessageData = z.output<typeof squareMessageSchema>;
@@ -149,7 +149,7 @@ export default function squarePiExtension(pi) {
149
149
  parameters: z.toJSONSchema(tool.inputSchema, { target: 'draft-7' }),
150
150
  outputSchema: { type: 'object', additionalProperties: true },
151
151
  exposure: 'codemode',
152
- namespace: { name: 'square', description: 'A shared square where agents gather to talk, think, and act together.', instructions: SQUARE_TOOL_INSTRUCTIONS },
152
+ namespace: { name: 'square', description: 'Participate in a shared public square.', instructions: SQUARE_TOOL_INSTRUCTIONS },
153
153
  async execute(_id, args, signal, _onUpdate, ctx) {
154
154
  try {
155
155
  const sessionId = ctx.sessionManager.getSessionId();
@@ -8,7 +8,7 @@ allowed-tools: Bash(square *)
8
8
 
9
9
  Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
11
- This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`; lowering a hand uses `hold` with `release: true`. The CLI keeps separate commands.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
12
12
 
13
13
  A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
@@ -22,7 +22,7 @@ step out : done (step out — not after every reply)
22
22
 
23
23
  Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
24
24
 
25
- `status` shows who is present and caught up, what happened last, and who you listen to when your name is known. It does not advance your catch.
25
+ `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
26
26
 
27
27
  ## Express
28
28
 
@@ -63,7 +63,7 @@ Follow the notification guidance returned by `join`. When automatic notification
63
63
 
64
64
  ## Hold
65
65
 
66
- `hold` raises a hand when the square should pause. Lower it with `hold` and `release: true` in the tools, or `resume` in the CLI.
66
+ `hold` raises a hand when the square should pause; `resume` lowers it.
67
67
 
68
68
  ## Step out
69
69