@astrosheep/square 0.3.88 → 0.3.90
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/artifact.js +11 -9
- package/dist/boundary-presentation.d.ts +3 -2
- package/dist/boundary-presentation.js +50 -77
- package/dist/delivery-deferral.d.ts +28 -0
- package/dist/delivery-deferral.js +83 -0
- package/dist/delivery-operations.js +8 -6
- package/dist/delivery.js +4 -1
- package/dist/inbox.js +21 -10
- package/dist/model.d.ts +1 -0
- package/dist/ports.d.ts +11 -3
- package/dist/presentation-operations.d.ts +20 -1
- package/dist/presentation-operations.js +68 -44
- package/dist/sqlite-square.js +29 -0
- package/dist/square-domain.d.ts +1 -1
- package/dist/square-domain.js +12 -0
- package/dist/square-facade.d.ts +8 -0
- package/dist/square-migration.d.ts +2 -1
- package/dist/square-migration.js +25 -26
- package/dist/square-sqlite.d.ts +5 -2
- package/dist/square-sqlite.js +79 -6
- package/dist/square-stdio.d.ts +1 -1
- package/dist/square-stdio.js +30 -17
- package/dist/stdio-server.js +23 -3
- package/guides/backends.md +26 -13
- package/guides/change-waits.md +40 -11
- package/guides/migration.md +30 -12
- package/guides/operation-cancellation.md +16 -1
- package/guides/participant-ownership.md +60 -186
- package/guides/square-api.md +72 -9
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +125 -185
- package/plugins/paseo/client/square-message.tsx +9 -25
- package/plugins/paseo/client/transform.ts +5 -4
- package/plugins/paseo/index.client.tsx +14 -12
- package/plugins/paseo/package.json +3 -0
- package/plugins/paseo/shared/message.ts +50 -1
|
@@ -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
|
-
|
|
7
|
+
export { parseSquareBlocks };
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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}>{
|
|
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
|
-
<
|
|
185
|
-
{
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
194
|
-
<View key={`${key}-${
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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,
|
|
222
|
-
<View key={
|
|
223
|
-
{
|
|
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
|
-
{
|
|
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 {
|
|
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,
|
|
11
|
+
const { text, items } = item.data;
|
|
12
12
|
const latest = items?.[items.length - 1];
|
|
13
|
-
//
|
|
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: "
|
|
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,
|
|
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
|
-
<
|
|
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={
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|
|
@@ -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(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/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>;
|