@astrosheep/square 0.3.95 → 0.3.97

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.d.ts +1 -1
  2. package/dist/artifact.js +11 -17
  3. package/dist/attention-presentation.d.ts +5 -4
  4. package/dist/attention-presentation.js +18 -8
  5. package/dist/boundary-presentation.js +31 -26
  6. package/dist/channel.d.ts +74 -0
  7. package/dist/channel.js +92 -0
  8. package/dist/claude-delivery.js +3 -9
  9. package/dist/cli/observation-commands.js +2 -2
  10. package/dist/codex-hook.d.ts +0 -2
  11. package/dist/codex-hook.js +0 -1
  12. package/dist/decisions.d.ts +2 -11
  13. package/dist/decisions.js +3 -8
  14. package/dist/delivery-health.js +3 -13
  15. package/dist/delivery.d.ts +1 -11
  16. package/dist/delivery.js +2 -9
  17. package/dist/identity.d.ts +0 -1
  18. package/dist/identity.js +0 -1
  19. package/dist/inbox.js +11 -7
  20. package/dist/mcp.js +65 -63
  21. package/dist/model.d.ts +6 -0
  22. package/dist/model.js +19 -0
  23. package/dist/notifications.d.ts +1 -1
  24. package/dist/ports.d.ts +5 -5
  25. package/dist/presentation-operations.d.ts +1 -0
  26. package/dist/sqlite-square.d.ts +3 -2
  27. package/dist/sqlite-square.js +91 -9
  28. package/dist/square-core.d.ts +1 -0
  29. package/dist/square-domain.js +22 -7
  30. package/dist/square-facade.d.ts +36 -10
  31. package/dist/square-sqlite.js +33 -4
  32. package/dist/square-tools.d.ts +1 -0
  33. package/dist/square-tools.js +9 -1
  34. package/dist/views.d.ts +1 -2
  35. package/dist/views.js +1 -9
  36. package/guides/operation-cancellation.md +5 -1
  37. package/guides/primitive-status-consolidation.md +1 -1
  38. package/guides/receive-primitive.md +4 -2
  39. package/guides/row-runtime-delivery-evidence.md +3 -3
  40. package/guides/row-storage-design.md +5 -1
  41. package/guides/square-api.md +63 -7
  42. package/package.json +5 -2
  43. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  44. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  45. package/plugins/codex/hooks/hooks.json +6 -6
  46. package/plugins/paseo/client/marble.ts +7 -0
  47. package/plugins/paseo/client/markdown.tsx +65 -9
  48. package/plugins/paseo/client/palette.ts +43 -20
  49. package/plugins/paseo/client/square-message.tsx +57 -48
  50. package/plugins/paseo/shared/message.ts +1 -2
  51. package/plugins/pi/square-pi.js +6 -4
@@ -1,5 +1,5 @@
1
1
  import { useMemo, type ReactNode } from "react";
2
- import { Platform, Text, View, type TextStyle, type ViewStyle } from "react-native";
2
+ import { Platform, StyleSheet, Text, View, type TextStyle } from "react-native";
3
3
  import type { PluginTheme } from "@getpaseo/plugin";
4
4
  import { Lexer, lexer, type Token, type Tokens } from "marked";
5
5
 
@@ -9,6 +9,7 @@ function makeStyles(colors: PluginTheme["colors"]) {
9
9
  return {
10
10
  block: { gap: 6 },
11
11
  text: { color: colors.foreground, fontSize: 13, lineHeight: 19 },
12
+ lead: { color: colors.accent, fontWeight: "600" as const },
12
13
  strong: { fontWeight: "500" as const, color: colors.foreground },
13
14
  em: { color: colors.foregroundMuted, fontStyle: "italic" as const },
14
15
  del: { color: colors.foregroundMuted, textDecorationLine: "line-through" as const },
@@ -117,7 +118,7 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
117
118
  default: {
118
119
  const block = token as Tokens.Generic;
119
120
  return (
120
- <Text key={key} style={[styles.text, textStyle]}>
121
+ <Text key={key} selectable={Platform.OS === "web"} style={[styles.text, textStyle]}>
121
122
  {renderInlines(block.tokens ?? Lexer.lexInline(block.text ?? block.raw), styles, key)}
122
123
  </Text>
123
124
  );
@@ -126,24 +127,79 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
126
127
  });
127
128
  }
128
129
 
130
+ /** The speaker run into the start of a line: the seal's blue, then the words. */
131
+ function Lead({ name, styles }: { name: string; styles: Styles }) {
132
+ return <Text style={styles.lead}>{name}{" "}</Text>;
133
+ }
134
+
129
135
  /** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
130
- export function SquareInline({ text, theme, numberOfLines, style }: {
136
+ export function SquareInline({ text, theme, numberOfLines, lead }: {
131
137
  text: string;
132
138
  theme: PluginTheme;
133
139
  numberOfLines?: number;
134
- style?: TextStyle;
140
+ lead?: string;
135
141
  }) {
136
142
  const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
137
143
  return (
138
- <Text numberOfLines={numberOfLines} style={[styles.text, style]}>
144
+ <Text numberOfLines={numberOfLines} style={styles.text}>
145
+ {lead !== undefined && <Lead name={lead} styles={styles} />}
139
146
  {renderInlines(Lexer.lexInline(text), styles, "inline")}
140
147
  </Text>
141
148
  );
142
149
  }
143
150
 
144
- /** A body of Markdown: no envelope, no route line — the words only, indented by the card. */
145
- export function SquareMarkdownBody({ text, theme, style }: { text: string; theme: PluginTheme; style?: ViewStyle }) {
151
+ /** A paragraph that is one *emphasis* and nothing else is an action, not a line. */
152
+ const isAction = (token: Token): token is Tokens.Paragraph =>
153
+ token.type === "paragraph" && children(token).length === 1 && children(token)[0]!.type === "em";
154
+
155
+ /** A bell or an action is stage direction: a caption on a hairline across the full width. */
156
+ export function SquareCue({ label, theme }: { label: ReactNode; theme: PluginTheme }) {
157
+ const rule = { flex: 1, height: StyleSheet.hairlineWidth, backgroundColor: theme.colors.border };
158
+ return (
159
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginVertical: 2 }}>
160
+ <View style={rule} />
161
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, lineHeight: 16, letterSpacing: 0.4 }}>{label}</Text>
162
+ <View style={rule} />
163
+ </View>
164
+ );
165
+ }
166
+
167
+ /**
168
+ * A body of Markdown: no envelope, no route line — the words only, at full width.
169
+ * `lead` runs the speaker into the first line; `actor` names whoever performs an action.
170
+ */
171
+ export function SquareMarkdownBody({ text, theme, lead, actor }: {
172
+ text: string;
173
+ theme: PluginTheme;
174
+ lead?: string;
175
+ actor?: string;
176
+ }) {
146
177
  const styles = useMemo(() => makeStyles(theme.colors), [theme.colors]);
147
- const tokens = useMemo(() => lexer(text), [text]);
148
- return <View style={[styles.block, style]}>{renderBlocks(tokens, styles, "body")}</View>;
178
+ const tokens = useMemo(() => lexer(text).filter(token => token.type !== "space"), [text]);
179
+ let pending = lead;
180
+ const nodes = tokens.map((token, index) => {
181
+ const key = `body-${index}`;
182
+ if (isAction(token)) {
183
+ const words = renderInlines(children(children(token)[0]!), styles, key);
184
+ return <SquareCue key={key} theme={theme} label={actor === undefined ? words : <>{actor} {words}</>} />;
185
+ }
186
+ if (pending === undefined) return renderBlocks([token], styles, key);
187
+ const name = pending;
188
+ pending = undefined;
189
+ if (token.type === "paragraph") {
190
+ return (
191
+ <Text key={key} selectable={Platform.OS === "web"} style={styles.text}>
192
+ <Lead name={name} styles={styles} />
193
+ {renderInlines(children(token), styles, key)}
194
+ </Text>
195
+ );
196
+ }
197
+ return [<Text key={`${key}-lead`} style={styles.text}><Lead name={name} styles={styles} /></Text>, ...renderBlocks([token], styles, key)];
198
+ });
199
+ return (
200
+ <View style={styles.block}>
201
+ {nodes}
202
+ {pending !== undefined && <Text style={styles.text}><Lead name={pending} styles={styles} /></Text>}
203
+ </View>
204
+ );
149
205
  }
@@ -1,40 +1,50 @@
1
1
  import type { PluginTheme } from "@getpaseo/plugin";
2
+ import { MARBLE_DARK, MARBLE_LIGHT } from "./marble";
2
3
 
3
4
  /**
4
- * Square's own courtyard palette: Carrara marble (grey-white stone with a
5
- * slate-blue vein), deliberately no relative of notesoup's warm parchment or
5
+ * Square's own courtyard palette: veinless honed stone (muted grey light, charcoal dark) and a
6
+ * slate seal ■ — everything half a step down, the quiet card on the timeline, deliberately no relative of notesoup's warm parchment or
6
7
  * pi-meantime's neutral carbon. Every token here is opaque and self-contained — the card and
7
8
  * Markdown never borrow the host's surface ramp or accent, only its appearance
8
9
  * (read from the host ink) and its accentForeground, which paints text on top
9
10
  * of an accent fill.
10
11
  */
11
12
  const DARK = {
12
- surface0: "#14171b", // inset / code
13
- surface1: "#1c2126", // card
14
- surface2: "#14171b", // inset / code
15
- border: "#2e353d",
16
- foreground: "#e8edf3",
17
- foregroundMuted: "#8b96a3",
18
- accent: "#8ea8c3", // plaza ■ / links
13
+ surface0: "#15171a", // inset / code
14
+ surface1: "#1a1d20", // card (charcoal stone)
15
+ surface2: "#15171a", // inset / code
16
+ border: "#2b2f34",
17
+ foreground: "#dde1e6",
18
+ foregroundMuted: "#858b94",
19
+ accent: "#8298ad", // plaza ■ / links
19
20
  } as const;
20
21
 
21
22
  const LIGHT = {
22
- surface0: "#e7ecf1",
23
- surface1: "#f2f5f9", // cool marble white
24
- surface2: "#e7ecf1",
25
- border: "#d2dae3",
26
- foreground: "#181d24",
27
- foregroundMuted: "#67737f",
28
- accent: "#4b6582",
23
+ surface0: "#e9ebee",
24
+ surface1: "#f4f5f6", // card (grey stone)
25
+ surface2: "#e9ebee",
26
+ border: "#e0e2e5",
27
+ foreground: "#2b2e33",
28
+ foregroundMuted: "#90959c",
29
+ accent: "#56687e",
29
30
  } as const;
30
31
 
31
- const VOICE = { dark: "#8b96a3", light: "#67737f" } as const; // grey: only the ■ carries colour
32
+ const TILE = 512;
33
+
34
+ /** Web-only surface: react-native-web passes these through as inline CSS. */
35
+ const STONE = {
36
+ // A slab resting on the page: one darker cut along the bottom edge, a tight contact shadow, no sheen.
37
+ dark: { image: `url(${MARBLE_DARK})`, edge: "#34393f", boxShadow: "0 1px 0 rgba(0,0,0,0.3), 0 4px 10px -6px rgba(0,0,0,0.55)", inlay: "inset 0 0 0 0.5px rgba(0,0,0,0.4)" },
38
+ light: { image: `url(${MARBLE_LIGHT})`, edge: "#c9ccd1", boxShadow: "0 1px 0 rgba(24,26,30,0.04), 0 4px 10px -6px rgba(24,26,30,0.18)", inlay: "inset 0 0 0 0.5px rgba(0,0,0,0.26)" },
39
+ } as const;
32
40
 
33
41
  export interface SquarePalette {
34
42
  /** Complete theme for the card and its Markdown surfaces. */
35
43
  theme: PluginTheme;
36
- /** The filled ● speaker dot; the hollow ○ keeps the muted ink instead. */
37
- voice: string;
44
+ /** Marble card surface for the web host, cut from the tile at `seed`; native keeps the flat surface1. */
45
+ stone(seed: string): { backgroundImage: string; backgroundSize: string; backgroundPosition: string; borderBottomColor: string; boxShadow: string };
46
+ /** Web-only: the ■ set into the stone. */
47
+ inlay: { boxShadow: string };
38
48
  }
39
49
 
40
50
  /** Paseo theme colors are hex (#RGB or #RRGGBB). */
@@ -57,6 +67,19 @@ export function squarePalette(theme: PluginTheme): SquarePalette {
57
67
  statusWarning: theme.colors.statusWarning,
58
68
  statusDanger: theme.colors.statusDanger,
59
69
  } },
60
- voice: dark ? VOICE.dark : VOICE.light,
70
+ stone: seed => {
71
+ // Each card shows its own patch of the slab, so neighbours never repeat a vein.
72
+ let hash = 2166136261;
73
+ for (const char of seed) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
74
+ const { image, edge, boxShadow } = dark ? STONE.dark : STONE.light;
75
+ return {
76
+ backgroundImage: image,
77
+ backgroundSize: `${TILE}px ${TILE}px`,
78
+ backgroundPosition: `${-((hash >>> 0) % TILE)}px ${-((hash >>> 9) % TILE)}px`,
79
+ borderBottomColor: edge,
80
+ boxShadow,
81
+ };
82
+ },
83
+ inlay: { boxShadow: (dark ? STONE.dark : STONE.light).inlay },
61
84
  };
62
85
  }
@@ -1,95 +1,104 @@
1
1
  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
- import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
4
+ import { Pressable, ScrollView, StyleSheet, Text, View, type ViewStyle } from "react-native";
5
5
  import { parseSquareBlocks, parseSquareVoice, type SquareMessageData } from "../shared/message";
6
- import { MONO, SquareInline, SquareMarkdownBody } from "./markdown";
6
+ import { MONO, SquareCue, SquareInline, SquareMarkdownBody } from "./markdown";
7
7
  import { squarePalette } from "./palette";
8
8
 
9
9
  /**
10
10
  * The accent ■ marks the square itself, not a route.
11
- * Collapsed: ■ who spoke · where · how many, then two preview lines.
12
- * Expanded: the same header, then the bodies indented under it. Several voices
13
- * become a stage script: each speaker holds a fixed left column, their words
14
- * run down a single ruled dialogue column. No narration.
11
+ * Header (both states): ■ square · who spoke … act ids · how many.
12
+ * Collapsed: two preview lines. Expanded: the whole scene, at full width.
13
+ * One attribution style for one voice or many — each speaker's name runs into
14
+ * their first line in the seal's color (not repeated while they keep talking);
15
+ * a bell or an *action* is stage direction, a caption on a hairline.
15
16
  */
16
17
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
17
18
  const [expanded, setExpanded] = useState(false);
19
+ const [hovered, setHovered] = useState(false);
18
20
  const palette = useMemo(() => squarePalette(theme), [theme]);
19
21
  const { text, items } = item.data;
20
22
  const latest = items?.[items.length - 1];
21
23
  const preview = latest?.preview?.replace(/\s+/g, " ").trim();
22
24
  const blocks = useMemo(() => parseSquareBlocks(text), [text]);
23
- const multi = blocks.length > 1;
24
- const single = multi ? undefined : parseSquareVoice(blocks[0].actor, blocks[0].meta);
25
+ const voices = useMemo(() => blocks.map(block => parseSquareVoice(block.actor, block.meta)), [blocks]);
25
26
  const names = items !== undefined
26
27
  ? items.map(entry => entry.actor)
27
- : blocks.map(block => parseSquareVoice(block.actor, block.meta).speaker);
28
+ : voices.map(voice => voice.speaker);
28
29
  const speakers = [...new Set(names.filter((name): name is string => name !== undefined))];
29
30
  const squareName = latest?.square ?? blocks[0].meta?.split("·")[1]?.trim() ?? "Square";
30
- const bell = latest?.route === "bell" || single?.bell === true;
31
+ const bell = latest?.route === "bell" || (voices.length === 1 && voices[0].bell);
31
32
  // Header is the same in both states; act id is always shown when we know it.
32
- const actIds = [...new Set(blocks.flatMap(block => {
33
- const id = parseSquareVoice(block.actor, block.meta).actId;
34
- return id ? [id] : [];
35
- }))];
36
- const trailing = `· ${squareName}${actIds.length ? ` · ${actIds.join(", ")}` : ""}${items && items.length > 1 ? ` · ${items.length}` : ""}`;
33
+ const actIds = [...new Set(voices.flatMap(voice => voice.actId ? [voice.actId] : []))];
34
+ const trailing = [actSpan(actIds), items && items.length > 1 ? String(items.length) : undefined]
35
+ .filter(Boolean).join(" · ");
37
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;
38
39
  const row = { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 };
39
40
  const indent = { paddingLeft: 15 };
40
41
 
41
42
  return (
42
- <View style={{ backgroundColor: surface1, borderWidth: StyleSheet.hairlineWidth, borderColor: border, borderRadius: 4, paddingHorizontal: 12, paddingVertical: 9 }}>
43
+ <View style={[
44
+ { backgroundColor: surface1, borderWidth: StyleSheet.hairlineWidth, borderColor: border, borderRadius: 4, paddingHorizontal: 12, paddingVertical: 9 },
45
+ layout.platform === "web" && palette.stone(`${squareName} ${actIds[0] ?? ""}`) as ViewStyle,
46
+ ]}>
43
47
  <Pressable
44
48
  accessibilityRole="button"
45
49
  accessibilityLabel={`${expanded ? "收起" : "展开"} ${squareName}: ${speakers.join(", ") || "Square"}`}
46
50
  aria-expanded={expanded}
47
51
  onPress={() => setExpanded(value => !value)}
48
- style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
52
+ onHoverIn={() => setHovered(true)}
53
+ onHoverOut={() => setHovered(false)}
54
+ style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })}
49
55
  >
50
56
  <View style={row}>
51
- <Text style={{ color: accent, fontSize: 9 }}>■</Text>
52
- <Text numberOfLines={1} style={{ color: foreground, fontSize: 13, fontWeight: "500", flex: 1 }}>
53
- {speakers.join(", ") || "Square"}
54
- {bell && <Text style={{ color: muted, fontWeight: "400" }}> · rang the bell</Text>}
57
+ <View style={[{ width: 9, height: 9, borderRadius: 1.5, backgroundColor: accent }, layout.platform === "web" && palette.inlay as ViewStyle]} />
58
+ <Text numberOfLines={1} style={{ color: foreground, fontSize: 13, fontWeight: "600", flex: 1 }}>
59
+ {squareName}
60
+ {(speakers.length > 0 || bell) && (
61
+ <Text style={{ color: muted, fontWeight: "400" }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
62
+ )}
55
63
  </Text>
56
- <Text style={{ color: muted, fontSize: 12 }}>{trailing}</Text>
57
- <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={15} color={muted} />
64
+ {trailing !== "" && <Text style={{ color: detail, fontSize: 11, fontFamily: MONO, fontVariant: ["tabular-nums"] }}>{trailing}</Text>}
65
+ <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={15} color={detail} />
58
66
  </View>
59
67
  {!expanded && preview !== undefined && preview !== "" && (
60
68
  <View style={{ ...indent, marginTop: 3 }}>
61
- <SquareInline text={preview} theme={palette.theme} numberOfLines={2} />
69
+ <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={latest?.actor} />
62
70
  </View>
63
71
  )}
64
72
  </Pressable>
65
73
  {expanded && (
66
74
  <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 3 }} nestedScrollEnabled>
67
- {multi ? (
68
- <View style={{ gap: 10 }}>
69
- {blocks.map((block, index) => {
70
- const voice = parseSquareVoice(block.actor, block.meta);
71
- const detail = [voice.bell ? "bell" : undefined, voice.actId].filter(Boolean).join(" · ");
72
- return (
73
- <View key={voice.actId ?? index} style={{ flexDirection: "row", alignItems: "flex-start", gap: 10 }}>
74
- <View style={{ width: 62, paddingTop: 1, gap: 1 }}>
75
- <View style={{ flexDirection: "row", alignItems: "center", gap: 5 }}>
76
- <Text style={{ fontSize: 8.5, color: voice.glyph === "○" ? muted : palette.voice }}>{voice.glyph}</Text>
77
- {voice.speaker && <Text numberOfLines={1} style={{ color: foreground, fontSize: 12.5, fontWeight: "500", flexShrink: 1 }}>{voice.speaker}</Text>}
78
- </View>
79
- {(voice.bell || voice.actId) && <Text numberOfLines={1} style={{ color: muted, fontSize: 10.5, fontFamily: MONO, paddingLeft: 13 }}>{detail}</Text>}
80
- </View>
81
- <View style={{ flex: 1, borderLeftWidth: StyleSheet.hairlineWidth, borderLeftColor: border, paddingLeft: 10 }}>
82
- <SquareMarkdownBody text={block.body} theme={palette.theme} />
83
- </View>
84
- </View>
85
- );
86
- })}
87
- </View>
88
- ) : (
89
- <SquareMarkdownBody text={blocks[0].body} theme={palette.theme} style={indent} />
90
- )}
75
+ <View style={{ ...indent, gap: 7 }}>
76
+ {blocks.map((block, index) => {
77
+ const voice = voices[index];
78
+ if (block.body === "" && !voice.bell) return null;
79
+ const repeat = index > 0 && voices[index - 1].speaker === voice.speaker;
80
+ return (
81
+ <View key={voice.actId ?? index} style={{ gap: 6 }}>
82
+ {voice.bell && <SquareCue theme={palette.theme} label={`${voice.speaker ?? ""} 🔔`} />}
83
+ <SquareMarkdownBody
84
+ text={block.body}
85
+ theme={palette.theme}
86
+ lead={repeat ? undefined : voice.speaker}
87
+ actor={voice.speaker}
88
+ />
89
+ </View>
90
+ );
91
+ })}
92
+ </View>
91
93
  </ScrollView>
92
94
  )}
93
95
  </View>
94
96
  );
95
97
  }
98
+
99
+ /** act/18, act/19 → act/18–19; anything else is listed as is. */
100
+ function actSpan(ids: string[]): string | undefined {
101
+ if (ids.length < 2) return ids[0];
102
+ const numbers = ids.map(id => /^act\/(\d+)$/.exec(id)?.[1]).map(Number);
103
+ return numbers.every(Number.isInteger) ? `act/${Math.min(...numbers)}–${Math.max(...numbers)}` : ids.join(", ");
104
+ }
@@ -50,7 +50,7 @@ export function parseSquareBlocks(text: string): SquareBlock[] {
50
50
  return blocks;
51
51
  }
52
52
 
53
- export interface SquareVoice { glyph: string; speaker?: string; bell: boolean; actId?: string }
53
+ export interface SquareVoice { speaker?: string; bell: boolean; actId?: string }
54
54
 
55
55
  // A voice line is chrome for the card: `● rei rang the bell` names who, how, and which
56
56
  // activity. Route prose is parsed into those marks, never rendered as narration.
@@ -58,7 +58,6 @@ export function parseSquareVoice(actorLine?: string, metaLine?: string): SquareV
58
58
  const line = actorLine?.trim() ?? "";
59
59
  const cleaned = line.replace(/^[●○◎◆■·]\s*/, "").trim();
60
60
  return {
61
- glyph: /^[●○◎·]/.exec(line)?.[0] ?? "●",
62
61
  speaker: /\S+/.exec(cleaned)?.[0],
63
62
  bell: /rang the bell/i.test(cleaned),
64
63
  actId: /act\/\d+/.exec(metaLine ?? "")?.[0],
@@ -3,7 +3,7 @@ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
3
3
  import { z } from 'zod/v4';
4
4
 
5
5
  import { Square, harnessOwner } from '../../dist/square-api.js';
6
- import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
6
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolsEnabled, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
7
7
 
8
8
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
9
9
  import { operationEnv } from '../../dist/participant-identity.js';
@@ -46,8 +46,10 @@ export function pendingCardDetails(pending) {
46
46
  recipient: membership.seat.participant,
47
47
  route: note.route,
48
48
  act: formatActivityId(note.actIndex),
49
- preview: previewAttentionBody(note.body),
50
- description: renderAttentionDescription({ actor: note.actor, route: note.route }),
49
+ preview: previewAttentionBody(note.body, note.delivery),
50
+ description: note.delivery?.mode === 'direct'
51
+ ? `${note.actor} → ${membership.seat.participant}`
52
+ : renderAttentionDescription({ actor: note.actor, route: note.route }),
51
53
  body: note.body,
52
54
  })));
53
55
  if (items.length === 0) return undefined;
@@ -141,7 +143,7 @@ export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
141
143
  }
142
144
 
143
145
  export default function squarePiExtension(pi) {
144
- for (const tool of SQUARE_TOOLS) {
146
+ if (squareToolsEnabled(process.env)) for (const tool of SQUARE_TOOLS) {
145
147
  pi.registerTool({
146
148
  name: `square_${tool.name}`,
147
149
  label: `Square ${tool.name}`,