@astrosheep/square 0.4.7 → 0.4.8
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/card-format.d.ts +2 -0
- package/dist/card-format.js +2 -0
- package/dist/claude-mcp-context.d.ts +0 -1
- package/dist/claude-mcp-context.js +0 -10
- package/dist/claude-mod.d.ts +2 -0
- package/dist/claude-mod.js +211 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +1 -12
- package/dist/cli/express-presentation.js +6 -33
- package/dist/cli/json.js +1 -1
- package/dist/cli/observation-commands.d.ts +2 -3
- package/dist/cli/observation-commands.js +21 -59
- package/dist/cli/registry.js +3 -5
- package/dist/cli/square-commands.d.ts +1 -2
- package/dist/cli/square-commands.js +7 -17
- package/dist/decisions.js +5 -4
- package/dist/harness-claude.js +3 -3
- package/dist/help.js +6 -8
- package/dist/mcp.js +1 -1
- package/dist/neutral.d.ts +15 -1
- package/dist/notifications.js +1 -1
- package/dist/operation-presentation.d.ts +17 -15
- package/dist/operation-presentation.js +58 -32
- package/dist/packages/agent-delivery/src/index.d.ts +4 -9
- package/dist/packages/agent-delivery/src/index.js +0 -9
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
- package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
- package/dist/packages/agent-delivery/src/pi-client.js +0 -2
- package/dist/packages/agent-delivery/src/types.d.ts +3 -37
- package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
- package/dist/plugins/paseo/shared/card-format.js +38 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
- package/dist/plugins/paseo/shared/card-presentation.js +3 -1
- package/dist/presentation.d.ts +2 -20
- package/dist/presentation.js +11 -46
- package/dist/sqlite-square.js +5 -6
- package/dist/square-facade.d.ts +3 -12
- package/dist/square-posting.js +1 -1
- package/dist/square-tools.d.ts +0 -11
- package/dist/square-tools.js +33 -119
- package/dist/square-values.d.ts +1 -1
- package/dist/square-values.js +1 -1
- package/dist/time.d.ts +1 -0
- package/dist/time.js +10 -0
- package/dist/views.d.ts +1 -1
- package/dist/views.js +2 -2
- package/dist/wake-transports.d.ts +1 -1
- package/dist/wake-transports.js +1 -1
- package/guides/catch-cold-path-optimization.md +1 -1
- package/guides/claude-native.md +37 -7
- package/guides/delivery-primitives.md +2 -1
- package/guides/mcp.md +3 -3
- package/guides/pi-native-delivery.md +10 -2
- package/guides/pi-tools.md +1 -3
- package/guides/row-storage-design.md +1 -1
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +3 -2
- package/plugins/claude/README.md +9 -0
- package/plugins/claude/hooks/hooks.json +1 -9
- package/plugins/claude/hooks/register.js +178 -0
- package/plugins/claude/skills/square/SKILL.md +2 -2
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/OVERVIEW.md +1 -1
- package/plugins/paseo/README.md +20 -23
- package/plugins/paseo/client/markdown.tsx +88 -95
- package/plugins/paseo/client/palette.ts +37 -12
- package/plugins/paseo/client/square-message.tsx +52 -76
- package/plugins/paseo/client/transform.ts +4 -6
- package/plugins/paseo/shared/card-format.ts +37 -0
- package/plugins/paseo/shared/card-presentation.ts +6 -2
- package/plugins/paseo/shared/message.ts +1 -6
- package/plugins/pi/square-pi.js +69 -50
- package/skills/square/SKILL.md +2 -2
- package/dist/claude-boundary-state.d.ts +0 -20
- package/dist/claude-boundary-state.js +0 -84
- package/dist/claude-hook.d.ts +0 -2
- package/dist/claude-hook.js +0 -232
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
- package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
- package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
- package/dist/packages/agent-delivery/src/claude-native.js +0 -75
- package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
- package/dist/packages/agent-delivery/src/claude.js +0 -66
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from "react";
|
|
2
|
-
import { Platform, Text, View, type TextStyle } from "react-native";
|
|
3
|
-
import type
|
|
2
|
+
import { Platform, ScrollView, Text, View, type TextStyle } from "react-native";
|
|
3
|
+
import { withAlpha, type SquarePalette } from "./palette";
|
|
4
4
|
import { Lexer, lexer, type Token, type Tokens } from "marked";
|
|
5
5
|
|
|
6
6
|
export const MONO = Platform.select({ ios: "Menlo", android: "monospace", default: "ui-monospace, Menlo, monospace" });
|
|
7
7
|
|
|
8
|
-
function makeStyles(
|
|
8
|
+
function makeStyles(palette: SquarePalette) {
|
|
9
|
+
const colors = palette.theme.colors;
|
|
10
|
+
const hairline = { borderColor: colors.border, borderWidth: 0.5 };
|
|
9
11
|
return {
|
|
10
|
-
block: { gap:
|
|
11
|
-
text: { color:
|
|
12
|
-
|
|
12
|
+
block: { gap: 8 },
|
|
13
|
+
text: { color: palette.body, fontSize: 13.5, lineHeight: 20 },
|
|
14
|
+
// Size carries only the top two levels; deeper headings are weight alone, so a card never shouts.
|
|
15
|
+
heading: { fontWeight: "600" as const, color: colors.foreground, letterSpacing: -0.1 },
|
|
16
|
+
headingSizes: [{ fontSize: 15.5, lineHeight: 22 }, { fontSize: 14.5, lineHeight: 21 }] as TextStyle[],
|
|
17
|
+
lead: { color: palette.name, fontWeight: "600" as const },
|
|
13
18
|
strong: { fontWeight: "600" as const, color: colors.foreground },
|
|
14
|
-
em: {
|
|
19
|
+
em: { fontStyle: "italic" as const },
|
|
15
20
|
del: { color: colors.foregroundMuted, textDecorationLine: "line-through" as const },
|
|
16
|
-
link: { color: colors.accent, textDecorationLine: "underline" as const },
|
|
17
|
-
inlineCode: { fontFamily: MONO },
|
|
18
|
-
codeBlock: { backgroundColor: colors.surface0, borderRadius: 4
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
link: { color: colors.accent, textDecorationLine: "underline" as const, textDecorationColor: withAlpha(colors.accent, 0.4) },
|
|
22
|
+
inlineCode: { fontFamily: MONO, fontSize: 12, color: colors.foreground, backgroundColor: palette.inlineCode, borderRadius: 3 },
|
|
23
|
+
codeBlock: { ...hairline, backgroundColor: colors.surface0, borderRadius: 4 },
|
|
24
|
+
codeContent: { paddingHorizontal: 10, paddingVertical: 8 },
|
|
25
|
+
codeText: { fontFamily: MONO, fontSize: 12, lineHeight: 18, color: colors.foreground },
|
|
26
|
+
quote: { borderLeftWidth: 2, borderLeftColor: colors.border, paddingLeft: 10, gap: 8 },
|
|
27
|
+
quoteText: { color: colors.foregroundMuted },
|
|
28
|
+
separator: { height: 0.5, backgroundColor: colors.border, marginVertical: 4 },
|
|
29
|
+
list: { gap: 4 },
|
|
30
|
+
listRow: { flexDirection: "row" as const },
|
|
31
|
+
// A fixed gutter gives wrapped lines a hanging indent; ordinals align on their right edge.
|
|
32
|
+
bullet: { color: colors.foregroundMuted, fontSize: 13.5, lineHeight: 20, minWidth: 18, paddingRight: 6,
|
|
33
|
+
textAlign: "right" as const, fontVariant: ["tabular-nums" as const] },
|
|
34
|
+
listItem: { flex: 1, gap: 4 },
|
|
35
|
+
table: { ...hairline, borderRadius: 4, overflow: "hidden" as const },
|
|
36
|
+
tableHead: { backgroundColor: colors.surface0 },
|
|
37
|
+
tableRow: { flexDirection: "row" as const, borderTopWidth: 0.5, borderTopColor: colors.border },
|
|
38
|
+
tableCell: { paddingVertical: 4, paddingHorizontal: 8 },
|
|
28
39
|
};
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -33,27 +44,27 @@ type Styles = ReturnType<typeof makeStyles>;
|
|
|
33
44
|
/** Children of a token, if it has any: `Token` is a union, so the optional field needs one cast. */
|
|
34
45
|
const children = (node: Token | { tokens?: Token[] }): Token[] => (node as { tokens?: Token[] }).tokens ?? [];
|
|
35
46
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
entity === "lt" ? "<" : entity === "gt" ? ">" : entity === "quot" ? '"' : entity === "#39" ? "'" : "&");
|
|
47
|
+
const ENTITIES: Record<string, string> = { lt: "<", gt: ">", quot: '"', "#39": "'", amp: "&" };
|
|
48
|
+
const unescape = (text: string): string => text.replace(/&(lt|gt|quot|#39|amp);/g, (_, entity: string) => ENTITIES[entity]!);
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
/** Inline runs nest inside their block's Text, so they carry only what differs from it. */
|
|
51
|
+
function renderInlines(tokens: Token[], styles: Styles, prefix: string): ReactNode[] {
|
|
41
52
|
return tokens.map((token, index) => {
|
|
42
53
|
const key = `${prefix}-${index}`;
|
|
43
|
-
const nested = (style
|
|
44
|
-
<Text key={key} style={
|
|
54
|
+
const nested = (style?: TextStyle): ReactNode => (
|
|
55
|
+
<Text key={key} style={style}>{renderInlines(children(token), styles, key)}</Text>
|
|
45
56
|
);
|
|
46
57
|
switch (token.type) {
|
|
47
58
|
case "strong": return nested(styles.strong);
|
|
48
59
|
case "em": return nested(styles.em);
|
|
49
60
|
case "del": return nested(styles.del);
|
|
50
61
|
case "link": return nested(styles.link);
|
|
51
|
-
case "codespan": return <Text key={key} style={
|
|
62
|
+
case "codespan": return <Text key={key} style={styles.inlineCode}>{` ${unescape(token.text)} `}</Text>;
|
|
52
63
|
case "br": return "\n";
|
|
53
64
|
case "image": return token.text;
|
|
54
65
|
default: {
|
|
55
66
|
const inline = token as Tokens.Generic;
|
|
56
|
-
if (inline.tokens !== undefined) return nested(
|
|
67
|
+
if (inline.tokens !== undefined) return nested();
|
|
57
68
|
return unescape(inline.text ?? inline.raw);
|
|
58
69
|
}
|
|
59
70
|
}
|
|
@@ -74,13 +85,13 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
74
85
|
return <View key={key} style={styles.separator} />;
|
|
75
86
|
case "code":
|
|
76
87
|
return (
|
|
77
|
-
<
|
|
88
|
+
<ScrollView key={key} horizontal style={styles.codeBlock} contentContainerStyle={styles.codeContent}>
|
|
78
89
|
<Text selectable style={styles.codeText}>{token.text}</Text>
|
|
79
|
-
</
|
|
90
|
+
</ScrollView>
|
|
80
91
|
);
|
|
81
92
|
case "heading":
|
|
82
93
|
return (
|
|
83
|
-
<Text key={key} style={[styles.text, textStyle, styles.
|
|
94
|
+
<Text key={key} style={[styles.text, textStyle, styles.heading, styles.headingSizes[(token as Tokens.Heading).depth - 1]]}>
|
|
84
95
|
{renderInlines(children(token), styles, key)}
|
|
85
96
|
</Text>
|
|
86
97
|
);
|
|
@@ -90,11 +101,11 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
90
101
|
const list = token as Tokens.List;
|
|
91
102
|
const start = typeof list.start === "number" ? list.start : 1;
|
|
92
103
|
return (
|
|
93
|
-
<View key={key} style={
|
|
104
|
+
<View key={key} style={styles.list}>
|
|
94
105
|
{list.items.map((item, itemIndex) => (
|
|
95
106
|
<View key={`${key}-${itemIndex}`} style={styles.listRow}>
|
|
96
107
|
<Text style={styles.bullet}>
|
|
97
|
-
{item.task ? (item.checked === true ? "☑" : "☐") : list.ordered ? `${start + itemIndex}.` : "
|
|
108
|
+
{item.task ? (item.checked === true ? "☑" : "☐") : list.ordered ? `${start + itemIndex}.` : "•"}
|
|
98
109
|
</Text>
|
|
99
110
|
<View style={styles.listItem}>{renderBlocks(children(item), styles, `${key}-${itemIndex}`, textStyle)}</View>
|
|
100
111
|
</View>
|
|
@@ -105,19 +116,26 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
105
116
|
case "table": {
|
|
106
117
|
const table = token as Tokens.Table;
|
|
107
118
|
const rows = [table.header, ...table.rows];
|
|
119
|
+
// Columns size to their widest cell (within reason) and the table scrolls sideways
|
|
120
|
+
// on narrow screens instead of crushing every cell to a sliver.
|
|
121
|
+
const widths = table.header.map((_, column) => Math.min(240, Math.max(56,
|
|
122
|
+
...rows.map(cells => (cells[column]?.text.length ?? 0) * 7.5 + 16))));
|
|
108
123
|
return (
|
|
109
|
-
<
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
<ScrollView key={key} horizontal>
|
|
125
|
+
<View style={styles.table}>
|
|
126
|
+
{rows.map((cells, rowIndex) => (
|
|
127
|
+
<View key={`${key}-${rowIndex}`} style={rowIndex === 0 ? [styles.tableRow, styles.tableHead, { borderTopWidth: 0 }] : styles.tableRow}>
|
|
128
|
+
{cells.map((cell, cellIndex) => (
|
|
129
|
+
<Text
|
|
130
|
+
key={`${key}-${rowIndex}-${cellIndex}`}
|
|
131
|
+
style={[styles.text, textStyle, styles.tableCell, { width: widths[cellIndex], textAlign: table.align[cellIndex] ?? "left" },
|
|
132
|
+
rowIndex === 0 ? styles.strong : undefined]}
|
|
133
|
+
>{renderInlines(children(cell), styles, `${key}-${rowIndex}-${cellIndex}`)}</Text>
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
))}
|
|
137
|
+
</View>
|
|
138
|
+
</ScrollView>
|
|
121
139
|
);
|
|
122
140
|
}
|
|
123
141
|
default: {
|
|
@@ -132,58 +150,33 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
132
150
|
});
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
/**
|
|
136
|
-
function
|
|
137
|
-
return <Text style={styles.lead}>{name}{"\u2002"}</Text>;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
|
|
141
|
-
export function SquareInline({ text, theme, numberOfLines, lead }: {
|
|
153
|
+
/** Attribution stays inline; a separate timestamp column reserves space on the first render. */
|
|
154
|
+
export function SquareMarkdownBody({ text, palette, author, reach, time }: {
|
|
142
155
|
text: string;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
palette: SquarePalette;
|
|
157
|
+
author?: string;
|
|
158
|
+
reach?: string;
|
|
159
|
+
time?: string;
|
|
146
160
|
}) {
|
|
147
|
-
const styles = useMemo(() => makeStyles(
|
|
148
|
-
return (
|
|
149
|
-
<Text numberOfLines={numberOfLines} style={styles.text}>
|
|
150
|
-
{lead !== undefined && <Lead name={lead} styles={styles} />}
|
|
151
|
-
{renderInlines(Lexer.lexInline(text), styles, "inline")}
|
|
152
|
-
</Text>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* A body of Markdown: no envelope — the words only, at full width.
|
|
158
|
-
* `lead` runs the author into the first line.
|
|
159
|
-
*/
|
|
160
|
-
export function SquareMarkdownBody({ text, theme, lead }: {
|
|
161
|
-
text: string;
|
|
162
|
-
theme: PluginTheme;
|
|
163
|
-
lead?: string;
|
|
164
|
-
}) {
|
|
165
|
-
const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
|
|
161
|
+
const styles = useMemo(() => makeStyles(palette), [palette]);
|
|
166
162
|
const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{pending !== undefined && <Text style={styles.text}><Lead name={pending} styles={styles} /></Text>}
|
|
187
|
-
</View>
|
|
188
|
-
);
|
|
163
|
+
const attribution = <>
|
|
164
|
+
{author !== undefined && <Text style={styles.lead}>{author}</Text>}
|
|
165
|
+
{reach !== undefined && <Text style={{ fontSize: 11, fontWeight: "500", color: palette.warm }}>{" "}{reach}</Text>}
|
|
166
|
+
{(author !== undefined || reach !== undefined) && "\u2002"}
|
|
167
|
+
</>;
|
|
168
|
+
const timeStyle: TextStyle = { color: palette.faint, fontSize: 11, lineHeight: 20, fontVariant: ["tabular-nums"] };
|
|
169
|
+
const first = tokens[0];
|
|
170
|
+
const runIn = first?.type === "paragraph" || first?.type === "heading";
|
|
171
|
+
const hasHeader = author !== undefined || reach !== undefined || time !== undefined;
|
|
172
|
+
const firstLine = <>{attribution}{runIn && renderInlines(children(first), styles, "first")}</>;
|
|
173
|
+
return <View style={styles.block}>
|
|
174
|
+
{hasHeader && <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
175
|
+
<Text selectable={Platform.OS === "web"} style={[styles.text, { flex: 1, minWidth: 0 }]}>
|
|
176
|
+
{firstLine}
|
|
177
|
+
</Text>
|
|
178
|
+
{time !== undefined && <Text style={[timeStyle, { flexShrink: 0 }]}>{time}</Text>}
|
|
179
|
+
</View>}
|
|
180
|
+
{renderBlocks(hasHeader && runIn ? tokens.slice(1) : tokens, styles, "body")}
|
|
181
|
+
</View>;
|
|
189
182
|
}
|
|
@@ -7,22 +7,22 @@ import type { PluginTheme } from "@getpaseo/plugin";
|
|
|
7
7
|
* the card earns its identity from colour and the seal, not decoration.
|
|
8
8
|
*/
|
|
9
9
|
const DARK = {
|
|
10
|
-
surface0: "#
|
|
10
|
+
surface0: "#1a1d21", // inset / code
|
|
11
11
|
surface1: "#252a2e", // card (matte slate paper)
|
|
12
|
-
surface2: "#
|
|
13
|
-
border: "#
|
|
14
|
-
foreground: "#
|
|
12
|
+
surface2: "#1a1d21", // inset / code
|
|
13
|
+
border: "#363d44",
|
|
14
|
+
foreground: "#e4e6ea",
|
|
15
15
|
foregroundMuted: "#858b94",
|
|
16
16
|
accent: "#94a6b5", // plaza ■ / links
|
|
17
17
|
} as const;
|
|
18
18
|
|
|
19
19
|
const LIGHT = {
|
|
20
|
-
surface0: "#
|
|
21
|
-
surface1: "#
|
|
22
|
-
surface2: "#
|
|
23
|
-
border: "#
|
|
24
|
-
foreground: "#
|
|
25
|
-
foregroundMuted: "#
|
|
20
|
+
surface0: "#e9edf0",
|
|
21
|
+
surface1: "#f6f7f9", // card (matte slate paper)
|
|
22
|
+
surface2: "#e9edf0",
|
|
23
|
+
border: "#dde2e7",
|
|
24
|
+
foreground: "#1f2226",
|
|
25
|
+
foregroundMuted: "#6b727b",
|
|
26
26
|
accent: "#71869a",
|
|
27
27
|
} as const;
|
|
28
28
|
|
|
@@ -33,7 +33,14 @@ export interface SquarePalette {
|
|
|
33
33
|
card: { borderRadius: number; borderWidth: number; boxShadow: string };
|
|
34
34
|
/** The square ■ seal, unrounded and flat. */
|
|
35
35
|
seal: { borderRadius: number };
|
|
36
|
-
/**
|
|
36
|
+
/** Body and metadata tones derived from the slate theme. */
|
|
37
|
+
body: string;
|
|
38
|
+
faint: string;
|
|
39
|
+
name: string;
|
|
40
|
+
warm: string;
|
|
41
|
+
directGuide: string;
|
|
42
|
+
inlineCode: string;
|
|
43
|
+
/** The thin message guide under the header. */
|
|
37
44
|
quote: { borderLeftWidth: number; borderLeftColor: string; paddingLeft: number };
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -45,6 +52,18 @@ function brightness(hex: string): number {
|
|
|
45
52
|
+ 0.114 * parseInt(rgb.slice(4, 6), 16);
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
export function withAlpha(hex: string, opacity: number): string {
|
|
56
|
+
const rgb = hex.length === 4 ? "#" + hex.slice(1).replace(/./g, digit => digit + digit) : hex.slice(0, 7);
|
|
57
|
+
return `${rgb}${Math.round(opacity * 255).toString(16).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mix(first: string, second: string, amount: number): string {
|
|
61
|
+
const rgb = (hex: string) => (hex.length === 4 ? hex.slice(1).replace(/./g, digit => digit + digit) : hex.slice(1, 7));
|
|
62
|
+
const a = rgb(first), b = rgb(second);
|
|
63
|
+
return "#" + [0, 2, 4].map(offset => Math.round(parseInt(a.slice(offset, offset + 2), 16) * amount
|
|
64
|
+
+ parseInt(b.slice(offset, offset + 2), 16) * (1 - amount)).toString(16).padStart(2, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
export function squarePalette(theme: PluginTheme): SquarePalette {
|
|
49
68
|
// The SDK exposes colors, not appearance. Use the host theme, never the OS color scheme.
|
|
50
69
|
const dark = brightness(theme.colors.foreground) > brightness(theme.colors.surface0);
|
|
@@ -59,6 +78,12 @@ export function squarePalette(theme: PluginTheme): SquarePalette {
|
|
|
59
78
|
} },
|
|
60
79
|
card: { borderRadius: 4, borderWidth: 0.5, boxShadow: "none" },
|
|
61
80
|
seal: { borderRadius: 0 },
|
|
62
|
-
|
|
81
|
+
body: mix(palette.foreground, palette.surface1, 0.9),
|
|
82
|
+
faint: mix(palette.foregroundMuted, palette.surface1, 0.7),
|
|
83
|
+
name: mix(palette.accent, palette.foreground, 0.8),
|
|
84
|
+
warm: theme.colors.statusDanger,
|
|
85
|
+
directGuide: withAlpha(theme.colors.statusDanger, 0.9),
|
|
86
|
+
inlineCode: withAlpha(palette.accent, 0.1),
|
|
87
|
+
quote: { borderLeftWidth: 1, borderLeftColor: withAlpha(theme.colors.statusWarning, 0.45), paddingLeft: 10 },
|
|
63
88
|
};
|
|
64
89
|
}
|
|
@@ -1,88 +1,64 @@
|
|
|
1
1
|
import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
|
|
2
2
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { cardReferences, reachLabel, timeLabel } from "../shared/card-format";
|
|
6
|
+
import type { SquareMessageData } from "../shared/message";
|
|
7
|
+
import { SquareMarkdownBody } from "./markdown";
|
|
7
8
|
import { squarePalette } from "./palette";
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* Collapsed: two preview lines. Expanded: the whole scene, at full width.
|
|
14
|
-
* Each structured message keeps its own author, reference and body.
|
|
15
|
-
*/
|
|
10
|
+
/** The first guide reaches 4px above its text, toward the seal; the collapsed window still shows 4 lines. */
|
|
11
|
+
const GUIDE_LEAD = 4;
|
|
12
|
+
const COLLAPSED_HEIGHT = 4 * 20 + GUIDE_LEAD;
|
|
13
|
+
|
|
16
14
|
export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
|
|
17
15
|
const [expanded, setExpanded] = useState(false);
|
|
18
16
|
const [hovered, setHovered] = useState(false);
|
|
17
|
+
const [overflows, setOverflows] = useState(false);
|
|
18
|
+
const scroll = useRef<ScrollView>(null);
|
|
19
19
|
const palette = useMemo(() => squarePalette(theme), [theme]);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
: latest?.preview ?? text;
|
|
27
|
-
const authors = [...new Set(presentations?.flatMap(entry => entry.author === undefined ? [] : [entry.author]))];
|
|
28
|
-
const chorus = authors.length > 1;
|
|
29
|
-
const origins = [...new Set(presentations?.map(entry => entry.origin))];
|
|
30
|
-
const originName = origins.join(", ") || "Notification";
|
|
31
|
-
const badge = presentations?.at(-1)?.badge;
|
|
32
|
-
// Exact references never imply intervening messages; each origin keeps its own references.
|
|
33
|
-
const trailing = [presentations?.flatMap(entry => entry.ref === undefined ? []
|
|
34
|
-
: [origins.length > 1 ? `${entry.origin} ${entry.ref}` : entry.ref]).join(", "),
|
|
35
|
-
presentations && presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(" · ");
|
|
36
|
-
const { accent, border, foreground, foregroundMuted: muted, surface1 } = palette.theme.colors;
|
|
37
|
-
// On web the header brightens its details under the cursor; touch keeps them quiet.
|
|
38
|
-
const detail = layout.platform === "web" && hovered ? foreground : muted;
|
|
39
|
-
const row = { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 };
|
|
40
|
-
const guide = { marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor };
|
|
20
|
+
const entries = "items" in item.data ? item.data.items : [{ text: item.data.text }];
|
|
21
|
+
const presentations = entries.filter(entry => "origin" in entry);
|
|
22
|
+
const originName = [...new Set(presentations.map(entry => entry.origin))].join(", ") || "Notification";
|
|
23
|
+
const refs = cardReferences(presentations);
|
|
24
|
+
const { accent, border, foregroundMuted: muted, surface1 } = palette.theme.colors;
|
|
25
|
+
const detail = layout.platform === "web" && hovered ? muted : palette.faint;
|
|
41
26
|
|
|
42
|
-
return (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<View style={{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<View key={index} style={index > 0 ? { paddingTop: 7, borderTopWidth: palette.card.borderWidth, borderTopColor: border } : undefined}>
|
|
78
|
-
{"text" in entry
|
|
79
|
-
? <SquareMarkdownBody text={entry.text} theme={palette.theme} />
|
|
80
|
-
: <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} />}
|
|
81
|
-
</View>
|
|
82
|
-
))}
|
|
83
|
-
</View>
|
|
84
|
-
</ScrollView>
|
|
85
|
-
)}
|
|
27
|
+
return <Pressable onPress={() => {
|
|
28
|
+
scroll.current?.scrollTo({ y: 0, animated: false });
|
|
29
|
+
setExpanded(value => !value);
|
|
30
|
+
}} accessibilityRole="button"
|
|
31
|
+
accessibilityLabel={`${expanded ? "Collapse" : "Expand"} ${originName}`} aria-expanded={expanded}
|
|
32
|
+
onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
|
|
33
|
+
style={{ backgroundColor: surface1, borderWidth: palette.card.borderWidth,
|
|
34
|
+
borderColor: border, borderRadius: palette.card.borderRadius, paddingVertical: 9, paddingHorizontal: 12 }}>
|
|
35
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 7, height: 16 }}>
|
|
36
|
+
<View style={{ width: 5, height: 5, backgroundColor: accent }} />
|
|
37
|
+
<Text numberOfLines={1} style={{ color: muted, fontSize: 11, fontWeight: "500", flexShrink: 1 }}>{originName}</Text>
|
|
38
|
+
{entries.length > 1 && <Text style={{ color: detail, fontSize: 11 }}>{entries.length} messages</Text>}
|
|
39
|
+
<View style={{ flex: 1 }} />
|
|
40
|
+
{!!refs && <Text numberOfLines={1} style={{ color: detail, fontSize: 11, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{refs}</Text>}
|
|
41
|
+
<View style={{ width: 10, height: 10 }}>
|
|
42
|
+
{overflows && <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />}
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
<View style={{ marginTop: 8 - GUIDE_LEAD, marginLeft: 2 }}>
|
|
46
|
+
<ScrollView ref={scroll} style={{ maxHeight: expanded ? 260 : COLLAPSED_HEIGHT }} scrollEnabled={expanded} nestedScrollEnabled
|
|
47
|
+
onContentSizeChange={(_, height) => setOverflows(height > COLLAPSED_HEIGHT)}>
|
|
48
|
+
{entries.map((entry, index) => {
|
|
49
|
+
const structured = "origin" in entry;
|
|
50
|
+
// A mention speaks through the guide colour alone; reply and bell keep their word.
|
|
51
|
+
const direct = structured && reachLabel(entry.reach) !== undefined;
|
|
52
|
+
const reach = structured && entry.reach !== "mention" ? reachLabel(entry.reach) : undefined;
|
|
53
|
+
return <View key={index} style={{ borderLeftWidth: 1,
|
|
54
|
+
borderLeftColor: direct ? palette.directGuide : palette.quote.borderLeftColor,
|
|
55
|
+
paddingLeft: palette.quote.paddingLeft, marginTop: index > 0 ? 8 : 0, paddingTop: index > 0 ? 0 : GUIDE_LEAD }}>
|
|
56
|
+
<SquareMarkdownBody text={structured ? entry.body : entry.text} palette={palette}
|
|
57
|
+
author={structured ? entry.author : undefined} reach={reach}
|
|
58
|
+
time={structured && entry.at !== undefined ? timeLabel(entry.at) : undefined} />
|
|
59
|
+
</View>;
|
|
60
|
+
})}
|
|
61
|
+
</ScrollView>
|
|
86
62
|
</View>
|
|
87
|
-
|
|
63
|
+
</Pressable>;
|
|
88
64
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
|
|
2
|
-
import {
|
|
2
|
+
import { OUTPUT_DISPLAY_MAX_CHARS, outputPreview, squareCardDetailsSchema, type SquareMessageData } 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.
|
|
@@ -12,17 +12,15 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
|
|
|
12
12
|
const details = squareCardDetailsSchema.safeParse(item.metadata.details);
|
|
13
13
|
let data: SquareMessageData;
|
|
14
14
|
if (details.success) {
|
|
15
|
-
const items = details.data.items
|
|
16
|
-
|
|
17
|
-
let remaining = OUTPUT_DISPLAY_MAX_CHARS - preview.length;
|
|
15
|
+
const items = details.data.items;
|
|
16
|
+
let remaining = OUTPUT_DISPLAY_MAX_CHARS;
|
|
18
17
|
const clipped = items.reduce((sum, entry) => sum + ("text" in entry ? entry.text : entry.body).length, 0) > remaining;
|
|
19
18
|
data = { items: items.map((entry, index) => {
|
|
20
19
|
const source = "text" in entry ? entry.text : entry.body;
|
|
21
20
|
const body = clipped ? outputPreview(source, Math.floor(remaining / (items.length - index))) : source;
|
|
22
21
|
remaining -= body.length;
|
|
23
22
|
if ("text" in entry) return { text: body };
|
|
24
|
-
|
|
25
|
-
return { ...presentation, body, ...(index === items.length - 1 ? { preview } : {}) };
|
|
23
|
+
return { ...entry, body };
|
|
26
24
|
}) };
|
|
27
25
|
} else {
|
|
28
26
|
data = { text: outputPreview(item.detail.text) };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CardPresentation } from "./card-presentation.ts";
|
|
2
|
+
|
|
3
|
+
export function reachLabel(reach?: CardPresentation["reach"]): string | undefined {
|
|
4
|
+
return reach === "mention" ? "@you" : reach === "attention" ? undefined : reach;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Calendar days, rather than elapsed hours, keep yesterday correct across DST. */
|
|
8
|
+
export function timeLabel(at: number, now = Date.now(), compact = false): string {
|
|
9
|
+
const date = new Date(at), today = new Date(now);
|
|
10
|
+
const day = (value: Date) => `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`;
|
|
11
|
+
const clock = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
12
|
+
if (day(date) === day(today)) return clock;
|
|
13
|
+
const yesterday = new Date(today);
|
|
14
|
+
yesterday.setDate(today.getDate() - 1);
|
|
15
|
+
if (day(date) === day(yesterday)) return `${compact ? "yest" : "yesterday"} ${clock}`;
|
|
16
|
+
const shortDate = compact
|
|
17
|
+
? `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`
|
|
18
|
+
: date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
19
|
+
return `${shortDate} ${clock}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Only adjacent references from the same origin can form a range. */
|
|
23
|
+
export function cardReferences(items: readonly CardPresentation[], originLabel: (origin: string) => string = origin => origin): string {
|
|
24
|
+
const multipleOrigins = new Set(items.map(item => item.origin)).size > 1;
|
|
25
|
+
const runs: { origin: string; ref: string; first?: number; last?: number }[] = [];
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
if (item.ref === undefined) continue;
|
|
28
|
+
const match = /^act\/(\d+)$/.exec(item.ref);
|
|
29
|
+
const index = match ? Number(match[1]) : undefined;
|
|
30
|
+
const previous = runs.at(-1);
|
|
31
|
+
if (index !== undefined && previous?.origin === item.origin && previous.last !== undefined && previous.last + 1 === index) {
|
|
32
|
+
previous.last = index;
|
|
33
|
+
} else runs.push({ origin: item.origin, ref: item.ref, first: index, last: index });
|
|
34
|
+
}
|
|
35
|
+
return runs.map(run => `${multipleOrigins ? `${originLabel(run.origin)} ` : ""}${run.first !== run.last
|
|
36
|
+
? `act/${run.first}–${run.last}` : run.ref}`).join(", ");
|
|
37
|
+
}
|
|
@@ -4,7 +4,9 @@ export type CardPresentation = {
|
|
|
4
4
|
readonly origin: string;
|
|
5
5
|
readonly author?: string;
|
|
6
6
|
readonly ref?: string;
|
|
7
|
-
|
|
7
|
+
/** Absent on cards produced before landing time and reach were recorded. */
|
|
8
|
+
readonly at?: number;
|
|
9
|
+
readonly reach?: 'mention' | 'reply' | 'attention' | 'bell';
|
|
8
10
|
readonly body: string;
|
|
9
11
|
};
|
|
10
12
|
|
|
@@ -12,5 +14,7 @@ export function isCardPresentation(value: unknown): value is CardPresentation {
|
|
|
12
14
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
13
15
|
const item = value as Record<string, unknown>;
|
|
14
16
|
return item.kind === 'presentation' && typeof item.origin === 'string' && typeof item.body === 'string'
|
|
15
|
-
&&
|
|
17
|
+
&& (item.at === undefined || (typeof item.at === 'number' && Number.isFinite(item.at)))
|
|
18
|
+
&& (item.reach === undefined || ['mention', 'reply', 'attention', 'bell'].includes(item.reach as string))
|
|
19
|
+
&& ['author', 'ref'].every(key => item[key] === undefined || typeof item[key] === 'string');
|
|
16
20
|
}
|
|
@@ -3,12 +3,10 @@ import { isCardPresentation, type CardPresentation } from "./card-presentation.t
|
|
|
3
3
|
|
|
4
4
|
/** Aggregate client display budget, including omission lines; source history stays unchanged. */
|
|
5
5
|
export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
|
|
6
|
-
export const COLLAPSED_PREVIEW_MAX_CHARS = 1_000;
|
|
7
6
|
export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS): string {
|
|
8
7
|
if (maxChars <= 0) return "";
|
|
9
8
|
if (text.length <= maxChars) return text;
|
|
10
9
|
const middle = "\n\n… Showing the beginning and end; the middle is hidden in this preview. …\n\n";
|
|
11
|
-
// Even a small share of a multi-message budget must fit, without splitting a surrogate pair.
|
|
12
10
|
if (maxChars <= middle.length) return text.slice(0, maxChars - 1).replace(/[\uD800-\uDBFF]$/, "") + "…";
|
|
13
11
|
const headChars = Math.floor((maxChars - middle.length) / 2);
|
|
14
12
|
const head = text.slice(0, headChars).replace(/[\uD800-\uDBFF]$/, "");
|
|
@@ -16,13 +14,10 @@ export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS)
|
|
|
16
14
|
return head + middle + tail;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
const presentationItemSchema = z.custom<CardPresentation>(isCardPresentation)
|
|
21
|
-
.and(z.object({ preview: z.string().optional() }));
|
|
17
|
+
const presentationItemSchema = z.custom<CardPresentation>(isCardPresentation);
|
|
22
18
|
export const squareCardDetailsSchema = z.object({
|
|
23
19
|
items: z.array(z.union([presentationItemSchema, z.object({ text: z.string() })])).min(1),
|
|
24
20
|
});
|
|
25
|
-
|
|
26
21
|
export const squareMessageSchema = z.union([
|
|
27
22
|
squareCardDetailsSchema,
|
|
28
23
|
z.object({ text: z.string() }),
|