@astrosheep/square 0.3.102 → 0.3.104
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/boundary-presentation.d.ts +4 -2
- package/dist/boundary-presentation.js +14 -14
- package/dist/claude-delivery.js +1 -1
- package/dist/delivery-attempts.js +1 -1
- package/dist/delivery-health.js +1 -1
- package/dist/inbox.js +1 -1
- package/dist/plugins/opencode/delivery.js +1 -1
- package/dist/ports.d.ts +4 -0
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +3 -3
- package/dist/square-domain.js +19 -0
- package/guides/delivery-primitives.md +31 -4
- package/guides/pi-native-delivery.md +10 -1
- 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/opencode/delivery.ts +1 -1
- package/plugins/paseo/README.md +18 -10
- package/plugins/paseo/client/markdown.tsx +4 -3
- package/plugins/paseo/client/palette.ts +25 -46
- package/plugins/paseo/client/square-message.tsx +37 -53
- package/plugins/paseo/client/transform.ts +16 -9
- package/plugins/paseo/shared/message.ts +14 -73
- package/plugins/pi/square-pi.js +4 -3
- package/plugins/paseo/client/marble.ts +0 -7
|
@@ -8,7 +8,9 @@ export declare function renderPendingAtBoundary(inbox: PendingMembership[]): str
|
|
|
8
8
|
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
9
9
|
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
10
10
|
* presentation evidence live. Every item is claimed and revalidated before the batch sink;
|
|
11
|
-
* after success, captured-seat commits record
|
|
11
|
+
* after success, captured-seat commits record the winning batch, including budget omissions as
|
|
12
|
+
* clipped (no consumption receipt), without an eligibility veto. Coverage is conservative if a
|
|
13
|
+
* competing claim's removal lets a previously omitted body fit in the final preview.
|
|
12
14
|
* A source that cannot verify its reuse must read the inbox again.
|
|
13
15
|
*/
|
|
14
|
-
export declare function presentPendingAtBoundary<T>(identity: Owner, present: (context: string) => T | Promise<T>, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
16
|
+
export declare function presentPendingAtBoundary<T>(identity: Owner, present: (context: string, delivered: PendingMembership[]) => T | Promise<T>, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -17,11 +17,14 @@ function renderBoundary(inbox) {
|
|
|
17
17
|
const agoraBlocks = [];
|
|
18
18
|
const deliveries = [];
|
|
19
19
|
let remainingAgora = queued.filter(({ notification }) => notification.delivery?.mode !== 'direct').length;
|
|
20
|
-
const
|
|
20
|
+
const entries = [];
|
|
21
21
|
let omitted = 0;
|
|
22
22
|
const omission = (count) => `${count} more ${count === 1 ? 'activity' : 'activities'} not shown.`;
|
|
23
23
|
for (const entry of queued) {
|
|
24
24
|
const { membership, notification } = entry;
|
|
25
|
+
// The omission line represents the rest of this batch too, but never consumes its bodies.
|
|
26
|
+
const coverage = { membership, actIndex: notification.actIndex, markSeen: false };
|
|
27
|
+
entries.push(coverage);
|
|
25
28
|
const block = renderAttentionPreview({
|
|
26
29
|
squarePath: membership.location,
|
|
27
30
|
actIndex: notification.actIndex,
|
|
@@ -43,15 +46,11 @@ function renderBoundary(inbox) {
|
|
|
43
46
|
}
|
|
44
47
|
blocks.push(block);
|
|
45
48
|
deliveries.push(notification.delivery);
|
|
46
|
-
|
|
47
|
-
membership,
|
|
48
|
-
actIndex: notification.actIndex,
|
|
49
|
-
markSeen: !attentionBodyIsClipped(notification.body, notification.delivery),
|
|
50
|
-
});
|
|
49
|
+
coverage.markSeen = !attentionBodyIsClipped(notification.body, notification.delivery);
|
|
51
50
|
}
|
|
52
51
|
return {
|
|
53
52
|
context: renderSquareReminder([...blocks, ...(omitted > 0 ? [omission(omitted)] : [])], deliveries),
|
|
54
|
-
|
|
53
|
+
entries,
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
56
|
async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sessionInbox, env = process.env, signal) {
|
|
@@ -66,7 +65,7 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
|
|
|
66
65
|
try {
|
|
67
66
|
try {
|
|
68
67
|
// Enumeration owns discovery/connections only; returned handles belong to the common runner.
|
|
69
|
-
for (const entry of renderBoundary(pending).
|
|
68
|
+
for (const entry of renderBoundary(pending).entries) {
|
|
70
69
|
const { location, seat } = entry.membership;
|
|
71
70
|
let artifact = artifacts.get(location);
|
|
72
71
|
if (artifact === undefined) {
|
|
@@ -87,12 +86,11 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
|
|
|
87
86
|
}
|
|
88
87
|
const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
|
|
89
88
|
const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
|
|
90
|
-
const
|
|
89
|
+
const delivered = pending.map(membership => ({ ...membership,
|
|
91
90
|
notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
|
|
92
|
-
})));
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}, present, { signal, classifyError: () => 'failed' });
|
|
91
|
+
})).filter(membership => membership.notifications.length > 0);
|
|
92
|
+
return { payload: { context: renderBoundary(delivered).context, delivered }, included: eligibleClaims };
|
|
93
|
+
}, payload => present(payload.context, payload.delivered), { signal });
|
|
96
94
|
if (report.observation.kind === 'threw')
|
|
97
95
|
throw report.observation.error;
|
|
98
96
|
for (const { result } of report.settlements)
|
|
@@ -110,7 +108,9 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
|
|
|
110
108
|
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
111
109
|
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
112
110
|
* presentation evidence live. Every item is claimed and revalidated before the batch sink;
|
|
113
|
-
* after success, captured-seat commits record
|
|
111
|
+
* after success, captured-seat commits record the winning batch, including budget omissions as
|
|
112
|
+
* clipped (no consumption receipt), without an eligibility veto. Coverage is conservative if a
|
|
113
|
+
* competing claim's removal lets a previously omitted body fit in the final preview.
|
|
114
114
|
* A source that cannot verify its reuse must read the inbox again.
|
|
115
115
|
*/
|
|
116
116
|
export async function presentPendingAtBoundary(identity, present, lookup = sessionInbox, env = process.env, signal) {
|
package/dist/claude-delivery.js
CHANGED
|
@@ -144,7 +144,7 @@ export async function observeClaudeDelivery(sessionId, text, operation, env = pr
|
|
|
144
144
|
if (operation === 'stored')
|
|
145
145
|
await presentPending({ artifact: row.artifact, location: request.location,
|
|
146
146
|
// Claude's stored event consumes even Agora previews; retain that contract for both modes.
|
|
147
|
-
participant: request.participant, activity: request.activity, seat: request.seat, markSeen: true, sink: { present() { } },
|
|
147
|
+
participant: request.participant, activity: request.activity, seat: request.seat, observationOf: request.claimToken, markSeen: true, sink: { present() { } },
|
|
148
148
|
current: facts => currentPayload(facts, request) === payload });
|
|
149
149
|
}
|
|
150
150
|
return { recognized, current: true };
|
|
@@ -64,7 +64,7 @@ export function presentationBlocksDispatch(attempts, now) {
|
|
|
64
64
|
finite(now);
|
|
65
65
|
return attempts.some(row => row.purpose === 'presentation'
|
|
66
66
|
&& (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
67
|
-
|| row.outcome === 'dispatching'
|
|
67
|
+
|| row.outcome === 'dispatching'));
|
|
68
68
|
}
|
|
69
69
|
/** An expired dispatch is uncertain, not permission for automatic replay. */
|
|
70
70
|
export function automaticDispatchEligible(attempts, now) {
|
package/dist/delivery-health.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function classifyDeliveryHealth(squarePath, opts) {
|
|
|
29
29
|
for (const { note, seat, evidence } of current) {
|
|
30
30
|
const ageMs = Math.max(0, now - note.at);
|
|
31
31
|
const presentation = evidence.some((row) => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
32
|
-
|| row.outcome === 'dispatching'
|
|
32
|
+
|| row.outcome === 'dispatching'));
|
|
33
33
|
const terminal = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown'));
|
|
34
34
|
const attemptRow = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'failed'));
|
|
35
35
|
const attempt = attemptRow === undefined || seat === undefined ? undefined : deliveryAttempt(squarePath, attemptRow, { squarePath, recipient: seat.participant, actIndex: note.index });
|
package/dist/inbox.js
CHANGED
|
@@ -27,7 +27,7 @@ function eligibleNotifications(facts, membership, now = Date.now()) {
|
|
|
27
27
|
const notifications = membership.notifications.filter(notification => {
|
|
28
28
|
if (cancelledThrough !== undefined && notification.actIndex <= cancelledThrough
|
|
29
29
|
|| facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(notification.actIndex))
|
|
30
|
-
.some(row => row.
|
|
30
|
+
.some(row => row.outcome !== 'failed' && row.outcome !== 'released'))
|
|
31
31
|
return false;
|
|
32
32
|
const deferredUntil = deliveryDeferralExpiry(facts, seat, notification.actIndex, now);
|
|
33
33
|
if (deferredUntil === undefined)
|
|
@@ -115,7 +115,7 @@ export async function observeOpenCodeContext(sessionId, messages, env, signal) {
|
|
|
115
115
|
if (signal.aborted || evidence.claimToken === undefined)
|
|
116
116
|
return;
|
|
117
117
|
await presentPending({ artifact, location, participant: seat.participant,
|
|
118
|
-
activity: formatActivityId(index), seat: asSeatRef(seat), sink: { present() { } }, markSeen: !clipped, signal,
|
|
118
|
+
activity: formatActivityId(index), seat: asSeatRef(seat), observationOf: evidence.claimToken, sink: { present() { } }, markSeen: !clipped, signal,
|
|
119
119
|
current: (facts) => {
|
|
120
120
|
const current = facts.seat(seat.participant);
|
|
121
121
|
const prepared = current === undefined || !sameSeat(current, seat) ? undefined : preview(facts, location, seat, index);
|
package/dist/ports.d.ts
CHANGED
|
@@ -133,6 +133,8 @@ export interface DeliveryClaimInput {
|
|
|
133
133
|
readonly seat: SeatRef;
|
|
134
134
|
readonly activity: ActivityId;
|
|
135
135
|
readonly kind: 'wake' | 'presentation';
|
|
136
|
+
/** Observe an existing wake's exact token; never authorizes another content injection. */
|
|
137
|
+
readonly observationOf?: string;
|
|
136
138
|
readonly route?: WakeAddress;
|
|
137
139
|
readonly at: number;
|
|
138
140
|
readonly leaseMs: number;
|
|
@@ -322,6 +324,8 @@ export interface PresentPendingInput {
|
|
|
322
324
|
readonly participant: string;
|
|
323
325
|
readonly activity: string | number;
|
|
324
326
|
readonly sink: PresentationSinkPort;
|
|
327
|
+
/** Existing wake observed by a native receiver; the sink must not inject content. */
|
|
328
|
+
readonly observationOf?: string;
|
|
325
329
|
/** Captured artifact seat; used to fence both presentation and consumption. */
|
|
326
330
|
readonly seat?: SeatRef;
|
|
327
331
|
readonly generation?: ActivityId;
|
|
@@ -14,5 +14,5 @@ export declare function preparePendingPresentation(input: Omit<PresentPendingInp
|
|
|
14
14
|
index: number;
|
|
15
15
|
at: number;
|
|
16
16
|
}> | undefined>;
|
|
17
|
-
/**
|
|
17
|
+
/** A sink error cannot prove that content was never injected. */
|
|
18
18
|
export declare function presentPending(input: PresentPendingInput): Promise<PresentationResult>;
|
|
@@ -36,7 +36,7 @@ export async function preparePendingPresentation(input) {
|
|
|
36
36
|
port: {
|
|
37
37
|
async claim(signal) {
|
|
38
38
|
const claimed = await input.artifact.claimDelivery({ seat: ref, activity: formatActivityId(index), kind: 'presentation',
|
|
39
|
-
at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, signal).catch(() => ({ status: 'stale' }));
|
|
39
|
+
observationOf: input.observationOf, at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, signal).catch(() => ({ status: 'stale' }));
|
|
40
40
|
return claimed.status === 'acquired' ? { token: claimToken, ordinal: claimed.attemptN } : undefined;
|
|
41
41
|
},
|
|
42
42
|
async current(signal) {
|
|
@@ -68,13 +68,13 @@ export async function preparePendingPresentation(input) {
|
|
|
68
68
|
},
|
|
69
69
|
}, { signal: input.signal });
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
71
|
+
/** A sink error cannot prove that content was never injected. */
|
|
72
72
|
export async function presentPending(input) {
|
|
73
73
|
const prepared = await preparePendingPresentation(input);
|
|
74
74
|
if (prepared === undefined)
|
|
75
75
|
return { presented: false };
|
|
76
76
|
const report = await presentPrepared(prepared, activity => input.sink.present(activity), {
|
|
77
|
-
signal: input.signal,
|
|
77
|
+
signal: input.signal,
|
|
78
78
|
});
|
|
79
79
|
if (report.observation.kind === 'threw')
|
|
80
80
|
throw report.observation.error;
|
package/dist/square-domain.js
CHANGED
|
@@ -248,6 +248,25 @@ export function wrapSquareWrite(read, writes, authority) {
|
|
|
248
248
|
},
|
|
249
249
|
claimDelivery(input) {
|
|
250
250
|
assertSeat(input.seat);
|
|
251
|
+
const evidence = read.deliveryEvidence(input.seat.participant, input.seat.generation, input.activity);
|
|
252
|
+
if (input.observationOf !== undefined) {
|
|
253
|
+
// Receiver adapters validate native identity/content before entering this transaction.
|
|
254
|
+
// This path records that existing message, rather than acquiring a second send.
|
|
255
|
+
if (input.kind !== 'presentation' || !evidence.some(row => row.kind === 'wake'
|
|
256
|
+
&& row.claimToken === input.observationOf && ['dispatching', 'accepted', 'unknown'].includes(row.outcome)))
|
|
257
|
+
return { status: 'stale' };
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const blocker = evidence.find(row => row.kind !== input.kind
|
|
261
|
+
&& row.outcome !== 'failed' && row.outcome !== 'released');
|
|
262
|
+
if (blocker !== undefined) {
|
|
263
|
+
if (blocker.outcome === 'dispatching' && blocker.expiresAt !== undefined && blocker.expiresAt <= input.at) {
|
|
264
|
+
// Normalize abandoned custody to unknown in the same transaction, never steal it.
|
|
265
|
+
return mutations.claimDelivery({ ...input, kind: blocker.kind });
|
|
266
|
+
}
|
|
267
|
+
return { status: blocker.outcome === 'dispatching' ? 'busy' : 'terminal', evidence: blocker };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
251
270
|
return mutations.claimDelivery(input);
|
|
252
271
|
},
|
|
253
272
|
prepareDelivery(input) {
|
|
@@ -52,6 +52,33 @@ currentness policy and the meaning of external proof remain the application's re
|
|
|
52
52
|
An `accepted` dispatch is not proof of presentation or consumption. Only the responsible presentation
|
|
53
53
|
boundary may assert `presented`; consuming an item is a separate application operation.
|
|
54
54
|
|
|
55
|
+
## Square content custody
|
|
56
|
+
|
|
57
|
+
Dispatch and presentation remain separate evidence series, but they are not independent permission
|
|
58
|
+
to inject content. Square's transaction-scoped domain gate checks both series for the same artifact,
|
|
59
|
+
recipient, seat generation and activity before admitting a new injection. A claim in either series
|
|
60
|
+
blocks the other; accepted, presented, clipped and unknown evidence also block it. Only failed or
|
|
61
|
+
released evidence permits another injection. An expired opposing claim is normalized to unknown in
|
|
62
|
+
that transaction rather than stolen. No lock is held over host or network I/O.
|
|
63
|
+
|
|
64
|
+
A native receiver that has verified the exact existing wake may pass its claim token as
|
|
65
|
+
`observationOf` to the presentation adapter. The gate verifies the wake belongs to the same captured
|
|
66
|
+
recipient/generation/activity and remains dispatching, accepted or unknown. This records observation
|
|
67
|
+
of that message; its sink must not inject another message. Claude and OpenCode retain their exact
|
|
68
|
+
native correlation/content checks. A successful wake alone never creates a consumption receipt.
|
|
69
|
+
Paseo currently has no corresponding native Pi observation correlation: its accepted/unknown attempt
|
|
70
|
+
stays unconsumed, available to explicit catch, without automatic native reinjection.
|
|
71
|
+
|
|
72
|
+
Session pending projections used for automatic injection suppress claimed, accepted and uncertain
|
|
73
|
+
items as well as presented ones. Failed/released attempts make them eligible again through delivery
|
|
74
|
+
revision observation. Native payload construction and exact-token observation read their captured
|
|
75
|
+
activity directly, not this injection queue. Cancellation and seat replacement retain their existing
|
|
76
|
+
fences. Uncertainty deliberately favors no automatic duplicate over automatic recovery; no
|
|
77
|
+
exactly-once processing or guaranteed eventual delivery is claimed.
|
|
78
|
+
|
|
79
|
+
These checks use the existing artifact evidence and transactions; no new registry or artifact format
|
|
80
|
+
is introduced. All running injectors must load the updated code before the guarantee applies.
|
|
81
|
+
|
|
55
82
|
## Transitions
|
|
56
83
|
|
|
57
84
|
An `Attempt<D>` has `purpose: 'dispatch' | 'presentation'`, `token`, zero-based `ordinal`, `outcome`,
|
|
@@ -83,8 +110,8 @@ The caller supplies valid stored attempts and consistently scoped, ordered rows.
|
|
|
83
110
|
`purpose`, `outcome`, and optional `expiresAt`; no dummy payload or identity is needed. These predicates
|
|
84
111
|
may see both purposes for the same logical delivery.
|
|
85
112
|
|
|
86
|
-
Presentation `presented`, `clipped`, `unknown`, or
|
|
87
|
-
presentation
|
|
113
|
+
Presentation `presented`, `clipped`, `unknown`, or `dispatching` blocks dispatch, including an
|
|
114
|
+
expired presentation claim: loss of its lease is uncertainty, not permission to send again. Automatic dispatch additionally requires no dispatch
|
|
88
115
|
`accepted`, `unknown`, or `dispatching`; even expired dispatch custody blocks automatic replay.
|
|
89
116
|
These are preselection predicates, not substitutes for a transactional claim or application gates.
|
|
90
117
|
|
|
@@ -173,8 +200,8 @@ supply isolation for a caller, so this is a mandatory port contract, tested in S
|
|
|
173
200
|
|
|
174
201
|
Presentation reports distinguish observed sink behavior from durable persistence. A returned sink
|
|
175
202
|
with `not-recorded` or `error` still returned successfully; that alone proves only the supplied sink's
|
|
176
|
-
documented effect, not model consumption or human display. Square
|
|
177
|
-
|
|
203
|
+
documented effect, not model consumption or human display. Square retains throwing sinks as unknown and rethrows the original exception after draining.
|
|
204
|
+
A missing native landing event or a canceled wait cannot prove the message never entered the host. Its single result stays
|
|
178
205
|
`{ presented: true, activity }` only on recorded success, false on ownership refusal; unexpected
|
|
179
206
|
success-settlement errors propagate. Batch returns its original sink value after all settlements,
|
|
180
207
|
with the same error propagation. No schema, storage format or public Square result changes are needed.
|
|
@@ -4,7 +4,16 @@ The shipped `plugins/pi/square-pi.js` remains the **only Square Pi extension**.
|
|
|
4
4
|
Both its activity steer and advisory `nextTurn` send call the SDK-free shared
|
|
5
5
|
`sendPiMessage` leaf compiled from `packages/agent-delivery/src`. Square keeps
|
|
6
6
|
framing, custom type, display, preview/UI, observation/presentation evidence,
|
|
7
|
-
|
|
7
|
+
uncertain-landing and canceled-batch suppression; the leaf neither schedules nor tracks.
|
|
8
|
+
|
|
9
|
+
Pi injection and external wake injection acquire mutually exclusive artifact-owned content custody.
|
|
10
|
+
An accepted or uncertain Paseo wake suppresses native Pi reinjection without asserting presentation
|
|
11
|
+
or consumption. Only proven failed/released attempts can return to the automatic queue. Missing
|
|
12
|
+
landing events, thrown sinks and expired claims do not authorize replay. Renderer details describe
|
|
13
|
+
only the batch that won custody, not the earlier observed inbox. Budget-omitted activities belong to
|
|
14
|
+
that same delivered batch: the omission line settles their presentation as `clipped`, without a
|
|
15
|
+
full-consumption receipt. They stay available to explicit catch, but never get paged into later
|
|
16
|
+
model completions or replayed on extension reload. See `delivery-primitives.md`.
|
|
8
17
|
|
|
9
18
|
For independent existing-session delivery, opt into its local receiver:
|
|
10
19
|
|
package/package.json
CHANGED
|
@@ -114,7 +114,7 @@ export async function observeOpenCodeContext(sessionId: string, messages: readon
|
|
|
114
114
|
for (const { evidence, index, hash, clipped } of matches) {
|
|
115
115
|
if (signal.aborted || evidence.claimToken === undefined) return;
|
|
116
116
|
await presentPending({ artifact, location, participant: seat.participant,
|
|
117
|
-
activity: formatActivityId(index), seat: asSeatRef(seat), sink: { present() {} }, markSeen: !clipped, signal,
|
|
117
|
+
activity: formatActivityId(index), seat: asSeatRef(seat), observationOf: evidence.claimToken, sink: { present() {} }, markSeen: !clipped, signal,
|
|
118
118
|
current: (facts) => {
|
|
119
119
|
const current = facts.seat(seat.participant);
|
|
120
120
|
const prepared = current === undefined || !sameSeat(current, seat) ? undefined : preview(facts, location, seat, index);
|
package/plugins/paseo/README.md
CHANGED
|
@@ -15,9 +15,9 @@ directory. Square tags up to v0.3.69 do not contain it.
|
|
|
15
15
|
Paseo supplies all runtime modules. No Git preparation step is needed. Plugins
|
|
16
16
|
are trusted, unsandboxed code running inside the Paseo app.
|
|
17
17
|
|
|
18
|
-
Requires Paseo >=0.11.2 with custom-message tool rows (#6090).
|
|
19
|
-
|
|
20
|
-
details
|
|
18
|
+
Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Structured notification
|
|
19
|
+
cards use Square's `details.items`, including each item's `actor`, `act`, `body` and
|
|
20
|
+
`route`. Missing or malformed details fall back to the original plain text.
|
|
21
21
|
|
|
22
22
|
## Display
|
|
23
23
|
|
|
@@ -25,13 +25,21 @@ Only synthetic `tool_call` items with `metadata.customType === "square"` and
|
|
|
25
25
|
plain-text detail are projected into a card. Real tools, other custom messages,
|
|
26
26
|
and unrecognized rows remain unchanged.
|
|
27
27
|
|
|
28
|
-
The
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
The heading and expanded messages use `metadata.details.items` directly, never
|
|
29
|
+
reverse-parsing notification envelopes or body text for senders, IDs or message
|
|
30
|
+
boundaries. Each expanded item keeps its full source body, author, exact activity ID
|
|
31
|
+
and bell marker, even when the model preview was clipped or omitted. IDs with gaps
|
|
32
|
+
are not compressed into a misleading range; items from different squares remain
|
|
33
|
+
separate. Collapsing shows only the last item's preview. Body text containing square
|
|
34
|
+
tags is ordinary content, not another notification.
|
|
35
|
+
|
|
36
|
+
Expanded bodies and the last collapsed preview share one 32,000 UTF-16-unit display
|
|
37
|
+
budget, including omission lines. The preview gets at most 1,000 units; the remaining
|
|
38
|
+
budget is shared among bodies. Long bodies retain their beginning and end, and
|
|
39
|
+
surrogate pairs are not split. Structured bodies are not also copied into `data.text`.
|
|
40
|
+
Joining context or malformed/missing details instead show bounded `detail.text`
|
|
41
|
+
under a generic heading, without inferring attribution. Original history and model
|
|
42
|
+
input remain unchanged.
|
|
35
43
|
Long content scrolls inside a bounded panel. The
|
|
36
44
|
card does not fetch extra activity or mark anything seen. Live events and fetched history use
|
|
37
45
|
the same transformer.
|
|
@@ -63,8 +63,9 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
63
63
|
case "space":
|
|
64
64
|
case "checkbox":
|
|
65
65
|
case "def":
|
|
66
|
-
case "html":
|
|
67
66
|
return null;
|
|
67
|
+
case "html":
|
|
68
|
+
return <Text key={key} selectable style={[styles.text, textStyle]}>{token.raw}</Text>;
|
|
68
69
|
case "hr":
|
|
69
70
|
return <View key={key} style={styles.separator} />;
|
|
70
71
|
case "code":
|
|
@@ -127,9 +128,9 @@ function renderBlocks(tokens: Token[], styles: Styles, prefix: string, textStyle
|
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
/**
|
|
131
|
+
/** A chorus speaker in seal blue, with a fixed half-em breath before the words. */
|
|
131
132
|
function Lead({ name, styles }: { name: string; styles: Styles }) {
|
|
132
|
-
return <Text style={styles.lead}>{name}{"
|
|
133
|
+
return <Text style={styles.lead}>{name}{"\u2002"}</Text>;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** One line (or a few) of Markdown, inline only: the collapsed preview on a notification card. */
|
|
@@ -1,50 +1,40 @@
|
|
|
1
1
|
import type { PluginTheme } from "@getpaseo/plugin";
|
|
2
|
-
import { MARBLE_DARK, MARBLE_LIGHT } from "./marble";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Square's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (read from the host ink) and its accentForeground, which paints text on top
|
|
10
|
-
* of an accent fill.
|
|
4
|
+
* Square's message card is a square-edged matte paper slip next to the timeline's
|
|
5
|
+
* frosted glass instruments: near-square corners, a square seal ■, a thin quote
|
|
6
|
+
* guide under the header. No glass sheen, no stone texture, no bottom cut —
|
|
7
|
+
* the card earns its identity from geometry, not decoration.
|
|
11
8
|
*/
|
|
12
9
|
const DARK = {
|
|
13
|
-
surface0: "#
|
|
14
|
-
surface1: "#
|
|
15
|
-
surface2: "#
|
|
16
|
-
border: "#
|
|
17
|
-
foreground: "#
|
|
10
|
+
surface0: "#14161a", // inset / code
|
|
11
|
+
surface1: "#252a2e", // card (matte slate paper)
|
|
12
|
+
surface2: "#14161a", // inset / code
|
|
13
|
+
border: "#3a4249",
|
|
14
|
+
foreground: "#d9dce1",
|
|
18
15
|
foregroundMuted: "#858b94",
|
|
19
|
-
accent: "#
|
|
16
|
+
accent: "#94a6b5", // plaza ■ / links
|
|
20
17
|
} as const;
|
|
21
18
|
|
|
22
19
|
const LIGHT = {
|
|
23
|
-
surface0: "#
|
|
24
|
-
surface1: "#
|
|
25
|
-
surface2: "#
|
|
26
|
-
border: "#
|
|
20
|
+
surface0: "#f4f5f7",
|
|
21
|
+
surface1: "#edf0f2", // card (matte slate paper)
|
|
22
|
+
surface2: "#f4f5f7",
|
|
23
|
+
border: "#ced5db",
|
|
27
24
|
foreground: "#2b2e33",
|
|
28
|
-
foregroundMuted: "#
|
|
29
|
-
accent: "#
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
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)" },
|
|
25
|
+
foregroundMuted: "#858b94",
|
|
26
|
+
accent: "#71869a",
|
|
39
27
|
} as const;
|
|
40
28
|
|
|
41
29
|
export interface SquarePalette {
|
|
42
30
|
/** Complete theme for the card and its Markdown surfaces. */
|
|
43
31
|
theme: PluginTheme;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
32
|
+
/** Flat slip: a hairline ring and no shadow, so the frosted rail beside it can carry the depth. */
|
|
33
|
+
card: { borderRadius: number; borderWidth: number; boxShadow: string };
|
|
34
|
+
/** The square ■ seal, unrounded and flat. */
|
|
35
|
+
seal: { borderRadius: number };
|
|
36
|
+
/** The thin quote guide under the header. */
|
|
37
|
+
quote: { borderLeftWidth: number; borderLeftColor: string; paddingLeft: number };
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
/** Paseo theme colors are hex (#RGB or #RRGGBB). */
|
|
@@ -67,19 +57,8 @@ export function squarePalette(theme: PluginTheme): SquarePalette {
|
|
|
67
57
|
statusWarning: theme.colors.statusWarning,
|
|
68
58
|
statusDanger: theme.colors.statusDanger,
|
|
69
59
|
} },
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 },
|
|
60
|
+
card: { borderRadius: 2, borderWidth: 0.5, boxShadow: "none" },
|
|
61
|
+
seal: { borderRadius: 0 },
|
|
62
|
+
quote: { borderLeftWidth: 1, borderLeftColor: dark ? "#45535e" : "#c2cdd5", paddingLeft: 9 },
|
|
84
63
|
};
|
|
85
64
|
}
|
|
@@ -1,38 +1,37 @@
|
|
|
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,
|
|
5
|
-
import {
|
|
4
|
+
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
5
|
+
import type { SquareMessageData } from "../shared/message";
|
|
6
6
|
import { MONO, SquareCue, SquareInline, SquareMarkdownBody } from "./markdown";
|
|
7
7
|
import { squarePalette } from "./palette";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* A square-edged matte slip next to the frosted glass instruments: near-square
|
|
11
|
+
* corners, square seal ■, thin quote guide under the header.
|
|
11
12
|
* Header (both states): ■ square · who spoke … act ids · how many.
|
|
12
13
|
* Collapsed: two preview lines. Expanded: the whole scene, at full width.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* a bell or an *action* is stage direction, a caption on a hairline.
|
|
14
|
+
* Each structured message keeps its own sender, activity id and body.
|
|
15
|
+
* A bell or an *action* is stage direction, a caption on a hairline.
|
|
16
16
|
*/
|
|
17
17
|
export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
|
|
18
18
|
const [expanded, setExpanded] = useState(false);
|
|
19
19
|
const [hovered, setHovered] = useState(false);
|
|
20
20
|
const palette = useMemo(() => squarePalette(theme), [theme]);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
.filter(Boolean).join(" · ");
|
|
21
|
+
const items = "items" in item.data ? item.data.items : undefined;
|
|
22
|
+
const text = "text" in item.data ? item.data.text : undefined;
|
|
23
|
+
const latest = items?.at(-1);
|
|
24
|
+
// Keep the source's own line breaks: collapsed and expanded text must wrap alike.
|
|
25
|
+
const preview = latest?.preview ?? text;
|
|
26
|
+
const speakers = [...new Set(items?.map(entry => entry.actor))];
|
|
27
|
+
const chorus = speakers.length > 1;
|
|
28
|
+
const squares = [...new Set(items?.map(entry => entry.square))];
|
|
29
|
+
const squareName = squares.join(", ") || "Square";
|
|
30
|
+
const bell = latest?.route === "bell";
|
|
31
|
+
// Exact IDs never imply intervening activities, and different squares keep their own IDs.
|
|
32
|
+
const actIds = items?.map(entry => entry.act) ?? [];
|
|
33
|
+
const trailing = [items?.map(entry => squares.length > 1 ? `${entry.square} ${entry.act}` : entry.act).join(", "),
|
|
34
|
+
items && items.length > 1 ? String(items.length) : undefined].filter(Boolean).join(" · ");
|
|
36
35
|
const { accent, border, foreground, foregroundMuted: muted, surface1 } = palette.theme.colors;
|
|
37
36
|
// On web the header brightens its details under the cursor; touch keeps them quiet.
|
|
38
37
|
const detail = layout.platform === "web" && hovered ? foreground : muted;
|
|
@@ -41,8 +40,8 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
|
|
|
41
40
|
|
|
42
41
|
return (
|
|
43
42
|
<View style={[
|
|
44
|
-
{ backgroundColor: surface1, borderWidth:
|
|
45
|
-
layout.platform === "web" &&
|
|
43
|
+
{ backgroundColor: surface1, borderWidth: palette.card.borderWidth, borderColor: border, borderRadius: palette.card.borderRadius, paddingHorizontal: 10, paddingVertical: 7 },
|
|
44
|
+
layout.platform === "web" && { boxShadow: palette.card.boxShadow },
|
|
46
45
|
]}>
|
|
47
46
|
<Pressable
|
|
48
47
|
accessibilityRole="button"
|
|
@@ -54,51 +53,36 @@ export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<S
|
|
|
54
53
|
style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })}
|
|
55
54
|
>
|
|
56
55
|
<View style={row}>
|
|
57
|
-
<View style={
|
|
58
|
-
<Text numberOfLines={1} style={{ color: foreground, fontSize:
|
|
56
|
+
<View style={{ width: 6, height: 6, borderRadius: palette.seal.borderRadius, backgroundColor: accent }} />
|
|
57
|
+
<Text numberOfLines={1} style={{ color: foreground, fontSize: 11.5, lineHeight: 17, fontWeight: "600", flex: 1 }}>
|
|
59
58
|
{squareName}
|
|
60
59
|
{(speakers.length > 0 || bell) && (
|
|
61
|
-
<Text style={{ color: muted, fontWeight: "400" }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
|
|
60
|
+
<Text style={{ color: muted, fontWeight: "400", fontSize: 10.5 }}> · {[speakers.join(", "), bell ? "🔔" : ""].filter(Boolean).join(" · ")}</Text>
|
|
62
61
|
)}
|
|
63
62
|
</Text>
|
|
64
|
-
{trailing !== "" && <Text style={{ color: detail, fontSize:
|
|
65
|
-
<Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={
|
|
63
|
+
{trailing !== "" && <Text numberOfLines={1} style={{ color: detail, fontSize: 9.5, fontFamily: MONO, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{trailing}</Text>}
|
|
64
|
+
<Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />
|
|
66
65
|
</View>
|
|
67
66
|
{!expanded && preview !== undefined && preview !== "" && (
|
|
68
|
-
<View style={{ ...indent, marginTop:
|
|
69
|
-
<SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={latest?.actor} />
|
|
67
|
+
<View style={{ ...indent, marginTop: 5, marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor }}>
|
|
68
|
+
<SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus ? latest?.actor : undefined} />
|
|
70
69
|
</View>
|
|
71
70
|
)}
|
|
72
71
|
</Pressable>
|
|
73
72
|
{expanded && (
|
|
74
73
|
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 3 }} nestedScrollEnabled>
|
|
75
74
|
<View style={{ ...indent, gap: 7 }}>
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
text={block.body}
|
|
85
|
-
theme={palette.theme}
|
|
86
|
-
lead={repeat ? undefined : voice.speaker}
|
|
87
|
-
actor={voice.speaker}
|
|
88
|
-
/>
|
|
89
|
-
</View>
|
|
90
|
-
);
|
|
91
|
-
})}
|
|
75
|
+
{items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
|
|
76
|
+
<View key={index} style={{ gap: 6 }}>
|
|
77
|
+
{entry.route === "bell"
|
|
78
|
+
? <SquareCue theme={palette.theme} label={`${entry.actor} 🔔 · ${entry.square} · ${entry.act}`} />
|
|
79
|
+
: <Text style={{ color: muted, fontSize: 11, fontFamily: MONO }}>{entry.square} · {entry.act}</Text>}
|
|
80
|
+
<SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.actor : undefined} actor={entry.actor} />
|
|
81
|
+
</View>
|
|
82
|
+
))}
|
|
92
83
|
</View>
|
|
93
84
|
</ScrollView>
|
|
94
85
|
)}
|
|
95
86
|
</View>
|
|
96
87
|
);
|
|
97
88
|
}
|
|
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
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
|
|
2
|
-
import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, squareCardDetailsSchema } from "../shared/message.ts";
|
|
2
|
+
import { COLLAPSED_PREVIEW_MAX_CHARS, 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.
|
|
@@ -10,19 +10,26 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
|
|
|
10
10
|
typeof item.detail.text !== "string") return;
|
|
11
11
|
|
|
12
12
|
const details = squareCardDetailsSchema.safeParse(item.metadata.details);
|
|
13
|
+
let data: SquareMessageData;
|
|
14
|
+
if (details.success) {
|
|
15
|
+
const items = details.data.items, latest = items.at(-1)!;
|
|
16
|
+
const preview = outputPreview(latest.preview ?? latest.body, COLLAPSED_PREVIEW_MAX_CHARS);
|
|
17
|
+
let remaining = OUTPUT_DISPLAY_MAX_CHARS - preview.length;
|
|
18
|
+
const clipped = items.reduce((sum, entry) => sum + entry.body.length, 0) > remaining;
|
|
19
|
+
data = { items: items.map(({ preview: _preview, ...entry }, index) => {
|
|
20
|
+
const body = clipped ? outputPreview(entry.body, Math.floor(remaining / (items.length - index))) : entry.body;
|
|
21
|
+
remaining -= body.length;
|
|
22
|
+
return { ...entry, body, ...(index === items.length - 1 ? { preview } : {}) };
|
|
23
|
+
}) };
|
|
24
|
+
} else {
|
|
25
|
+
data = { text: outputPreview(item.detail.text) };
|
|
26
|
+
}
|
|
13
27
|
return {
|
|
14
28
|
items: [{
|
|
15
29
|
type: "plugin",
|
|
16
30
|
kind: "square-message",
|
|
17
31
|
version: 1,
|
|
18
|
-
data
|
|
19
|
-
text: outputPreview(details.success && details.data.fullText !== undefined ? details.data.fullText : item.detail.text),
|
|
20
|
-
...(details.success ? {
|
|
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) }),
|
|
24
|
-
} : {}),
|
|
25
|
-
},
|
|
32
|
+
data,
|
|
26
33
|
}],
|
|
27
34
|
};
|
|
28
35
|
};
|
|
@@ -1,95 +1,36 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Aggregate client display budget, including omission lines; source history stays unchanged. */
|
|
4
4
|
export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
|
|
5
5
|
export const COLLAPSED_PREVIEW_MAX_CHARS = 1_000;
|
|
6
6
|
export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS): string {
|
|
7
|
+
if (maxChars <= 0) return "";
|
|
7
8
|
if (text.length <= maxChars) return text;
|
|
8
9
|
const middle = "\n\n… Showing the beginning and end; the middle is hidden in this preview. …\n\n";
|
|
10
|
+
// Even a small share of a multi-message budget must fit, without splitting a surrogate pair.
|
|
11
|
+
if (maxChars <= middle.length) return text.slice(0, maxChars - 1).replace(/[\uD800-\uDBFF]$/, "") + "…";
|
|
9
12
|
const headChars = Math.floor((maxChars - middle.length) / 2);
|
|
10
|
-
// Budget in UTF-16 units, but never split a surrogate pair.
|
|
11
13
|
const head = text.slice(0, headChars).replace(/[\uD800-\uDBFF]$/, "");
|
|
12
14
|
const tail = text.slice(-(maxChars - middle.length - headChars)).replace(/^[\uDC00-\uDFFF]/, "");
|
|
13
15
|
return head + middle + tail;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
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
|
-
|
|
53
|
-
export interface SquareVoice { speaker?: string; bell: boolean; actId?: string }
|
|
54
|
-
|
|
55
|
-
// A voice line is chrome for the card: `● rei rang the bell` names who, how, and which
|
|
56
|
-
// activity. Route prose is parsed into those marks, never rendered as narration.
|
|
57
|
-
export function parseSquareVoice(actorLine?: string, metaLine?: string): SquareVoice {
|
|
58
|
-
const line = actorLine?.trim() ?? "";
|
|
59
|
-
const cleaned = line.replace(/^[●○◎◆■·]\s*/, "").trim();
|
|
60
|
-
return {
|
|
61
|
-
speaker: /\S+/.exec(cleaned)?.[0],
|
|
62
|
-
bell: /rang the bell/i.test(cleaned),
|
|
63
|
-
actId: /act\/\d+/.exec(metaLine ?? "")?.[0],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
18
|
+
/** Renderer facts emitted by pendingCardDetails, never derived from body or route prose. */
|
|
67
19
|
export const squareCardDetailsSchema = z.object({
|
|
68
|
-
fullText: z.string().optional(),
|
|
69
20
|
items: z.array(z.object({
|
|
70
21
|
square: z.string(),
|
|
71
|
-
actor: z.string()
|
|
22
|
+
actor: z.string(),
|
|
23
|
+
act: z.string().regex(/^act\/(0|[1-9]\d*)$/),
|
|
24
|
+
body: z.string(),
|
|
72
25
|
recipient: z.string().optional(),
|
|
73
|
-
route: z.enum(["mention", "reply", "attention", "bell"])
|
|
26
|
+
route: z.enum(["mention", "reply", "attention", "bell"]),
|
|
74
27
|
preview: z.string().optional(),
|
|
75
|
-
description: z.string(),
|
|
76
28
|
})).min(1),
|
|
77
29
|
});
|
|
78
30
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
switch (item.route) {
|
|
84
|
-
case "mention": return to === undefined ? `${item.actor} spoke your name` : `${item.actor} spoke ${to}'s name`;
|
|
85
|
-
case "reply": return to === undefined ? `${item.actor} answered you` : `${item.actor} answered ${to}`;
|
|
86
|
-
case "attention": return to === undefined ? `${item.actor} spoke · you're listening` : `${item.actor} spoke · ${to} is listening`;
|
|
87
|
-
case "bell": return `${item.actor} rang the bell`;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const squareMessageSchema = z.object({
|
|
92
|
-
text: z.string(),
|
|
93
|
-
items: squareCardDetailsSchema.shape.items.optional(),
|
|
94
|
-
});
|
|
31
|
+
export const squareMessageSchema = z.union([
|
|
32
|
+
squareCardDetailsSchema,
|
|
33
|
+
z.object({ text: z.string() }),
|
|
34
|
+
]);
|
|
95
35
|
export type SquareMessageData = z.output<typeof squareMessageSchema>;
|
|
36
|
+
export type SquareNotificationItem = z.output<typeof squareCardDetailsSchema>["items"][number];
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -383,7 +383,7 @@ export default function squarePiExtension(pi) {
|
|
|
383
383
|
try {
|
|
384
384
|
await presentPendingAtBoundary(
|
|
385
385
|
harnessOwner({ channel: 'pi', session: boundSessionId }),
|
|
386
|
-
async (content) => {
|
|
386
|
+
async (content, delivered) => {
|
|
387
387
|
if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
|
|
388
388
|
// One card for the human: a compact heading, expandable to the full body.
|
|
389
389
|
// Model context stays bounded; renderer-only details carry the complete text.
|
|
@@ -391,7 +391,7 @@ export default function squarePiExtension(pi) {
|
|
|
391
391
|
const landing = waitForLanding(messageContent, signal, 'steer');
|
|
392
392
|
try {
|
|
393
393
|
sendPiMessage(pi,
|
|
394
|
-
{ customType: 'square', content: messageContent, display: true, details: pendingCardDetails(
|
|
394
|
+
{ customType: 'square', content: messageContent, display: true, details: pendingCardDetails(delivered) },
|
|
395
395
|
{ deliverAs: 'steer', triggerTurn: true },
|
|
396
396
|
);
|
|
397
397
|
} catch (error) {
|
|
@@ -411,7 +411,8 @@ export default function squarePiExtension(pi) {
|
|
|
411
411
|
signal,
|
|
412
412
|
);
|
|
413
413
|
if (signal.aborted || token !== generation) return;
|
|
414
|
-
|
|
414
|
+
// Only committed evidence suppresses successful delivery. A competing wake may
|
|
415
|
+
// have won the claim and can still fail before sending, making this item eligible again.
|
|
415
416
|
} catch (error) {
|
|
416
417
|
if (signal.aborted || token !== generation) return;
|
|
417
418
|
if (error instanceof PiDeliveryDroppedError) {
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seamless 512px stone tiles, rendered once offline (float canvas noise, dithered, JPEG):
|
|
3
|
-
* veinless honed stone — only the milky drift, on muted grey-white (light) or charcoal (dark).
|
|
4
|
-
* Kept tiny so they inline as data URIs.
|
|
5
|
-
*/
|
|
6
|
-
export const MARBLE_LIGHT = "data:image/jpeg;base64,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";
|
|
7
|
-
export const MARBLE_DARK = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAYEBAUEBAYFBQUGBgYHCQ4JCQgICRINDQoOFRIWFhUSFBQXGiEcFxgfGRQUHScdHyIjJSUlFhwpLCgkKyEkJST/2wBDAQYGBgkICREJCREkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCT/wAARCAIAAgADASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECCP/EABYQAQEBAAAAAAAAAAAAAAAAAAABEf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A54AVFEAUQAAAAAAAAAAAAAAAABVRQWKkAURVBFARQQAAAAQAEABEUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFiLAVUigKCgAgAgKIKKCIAAJUVAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUAVWV0GtNTTQaE00FEFFQEABQAQQABFARGkBMFKCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAumoA1oigoigAgKIoAACKgAAAKCFioDNFqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKigoCgaIgoigogAAAAAqAKgAgqUEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYJFBQAEVAAAU1AFQAAAFRQAUESqKMi1EAAAXDAQXAEFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACKkagAoCCgIACAAgAKIoCooACgAgYYqKJgogAAAqiIogyKAguIAAAKgAAAAAAAAAAAAAAAAAAAAAAAAAAEaZUGgAAAEUBEVAQVAURQURQAAAAUAABRBRADAAVAQVAQVARQAQAAAAAAAAAAAAAAAAAAAAAAAAAAAUAFEFF0QQUEAABAAAAURQUAAAFEUAFUAEAAAAEFQERUAAARUAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAFEUAEAAAAAAAVFBQAAAAAVWVBTTUBRFAAARUBBUBBUBAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAVFBRFAABAAUAFAAABRAFQQFSiACAAAAAAAAAAAAAAAAEBQigIoCCmAiNICAAAAAAAAAAAAAAAAAAAAsEUAAAAFEUFEAVABRAAAEqKgAAAAAAAAAAAAAAAAAALFSAKAAqAKgAgqAgAAAAAAAAAAAAAAAAAAAAAAAKIoKIoCooCKAiKAiKAgAAAAAAAAAAAAAAAAACoAqoAogCgAAAlRagAAAAAAAAAAAAAAAAAAAAAAAAKqRQFAAEARUAQAAAAAAAAAAAAAAAAAAAAAAAXRAFNQBoQAqAAAAAAAAAAAAAAAAAAAAAAAAACxUgDQmgACglVEBAAAAAAAAAAAAAAAAAAAAAAAAAAABRFBAAAAAAAAAAAAAAAAAAAAAAAAAAAAU1DQXTUAXRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVFBBQEFAQAAAAAAAAAAAAAAAFAAABBUAAABQRQAAAAAAARUAAAAAAAAAAAAAAAAAAAAABQAUEFAQVAQVAAAAAAAAAAAAAAAUAAAAAAxTATBQEFQAAAEBRAAAAAAAAAAAAAAAAAAAAAAABUUBUUAUBBQEwxQGcRpAQAAAAAAAAABUAURQBQAFAAABVGRUQRGqgIAAAAAAAAAAAAAAAAAAAAAAAAAChi4AKAAqiAIAAIACIoCCoAAAAAAACgAsAXBYCKKCC4AgoCIoCM1pFGaLYiAAAAAAAAAAAAAACooCCgIKmACgILhgNYYoACggACLUABAAAQAEAAAAAAFAAAFiKCxYiwFCAAAAoohioCJWkBmo0iCIoCCgIAAAAAAAACgAoIooJiY0YDKrgCLgqigoIKAiNIDIqICKAgUARQERQEAAUAFAAUBBQBQAVBRQAFQBUABFQBFQERRBEVAAAAAAAAAFiKCgoAKAAAACKALFZVRQAEBBEVKCAKACACAIqAAoAAKqAKAACgAoIKigoIAgoAIAAAIoJiiCVlpKCAAAAAAAAKigsVAFABRFABFFEEFisqCiCioCCBUAAAAAQKCAAKigAAKigoAKEVQBAURQEAAAAQQFQBUAAAERQERUAAAAAABRFBQAAAFQBRNUARQFRQAABACoACCAuiAKgAAAAAoACgAoAoigCCiiAKIIAAAgCgAAAJVQBABKAAAAAAAAqAKqQBQAAQFEAURQFZAaEAUQARagCAAAAAAAAAAqKAqAKqKACqIoAgCAigIAACAqoAoigIqAgIAAAAAAAAAAAACiAKgAaaALpqACoAqooAACCUAAAAAAAAAAAABUUFVFAUAEVAAFAUQRFAQVAAAAAEABFQAAAAAAAAAAAAAAAAAAAAAAFAAAARUAAAAAAAAAAAAAABqKysBoRQAFEAQAVRBQEMUQQAERUAQAAAAAAAAAAAAAAAAAAAAAAAf/9k=";
|