@astrosheep/square 0.3.95 → 0.3.96

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.
@@ -1,16 +1,17 @@
1
- import { formatActivityId } from './square-core.js';
1
+ import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
2
2
  import { assertCurrentSeat } from './square-domain.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
4
  import { validateMetadataMatch, validateDeferralDuration, validateDeferralId } from './delivery-deferral.js';
5
5
  import { squareLocation } from './square-location.js';
6
6
  import crypto from 'node:crypto';
7
7
  import { setTimeout as sleep } from 'node:timers/promises';
8
- import { InternalSquareError, SquareError, isSquareError, nameKey, validateName } from './model.js';
8
+ import { activityMetadataHasContent, InternalSquareError, SquareError, isSquareError, nameKey, sameName, validateActivityMetadata, validateName } from './model.js';
9
9
  import { coreDone, coreIgnore, coreListen, coreListening, coreUnlisten, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
10
  import { WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
11
11
  import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
12
12
  import { expressionContext } from './activity-feed.js';
13
13
  import { expressHint } from './hints.js';
14
+ import { ATTENTION_BODY_MAX } from './attention-presentation.js';
14
15
  import { asSeatRef, participantAuthority, requireOwner, requireJoinName } from './seats.js';
15
16
  import { recoveryObservation, entryPresentation, snapshot, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
16
17
  import { sessionInboxCounts } from './inbox.js';
@@ -133,6 +134,7 @@ export class SqliteSquare {
133
134
  this.owner = owner;
134
135
  this.seat = seat;
135
136
  }
137
+ get generation() { return this.seat.generation; }
136
138
  async express(bodyOrInput, optionsOrControl = {}, operationControl) {
137
139
  const square = this.owner;
138
140
  const input = typeof bodyOrInput === 'object' && bodyOrInput !== null ? bodyOrInput : undefined;
@@ -382,6 +384,56 @@ export class SqliteSquare {
382
384
  }, control?.signal)).value;
383
385
  }
384
386
  };
387
+ /** Direct messages use explicit recipients, not participant expression admission or body attention. */
388
+ async send(input, control) {
389
+ checkControl(control);
390
+ const { from, to, body, truncate = ATTENTION_BODY_MAX } = input;
391
+ if (typeof from !== 'string')
392
+ throw new SquareError('invalid_args', 'Channel sender must be a name.');
393
+ validateName(from);
394
+ const targets = typeof to === 'string' ? [to] : to;
395
+ if (!Array.isArray(targets) || targets.length < 1 || targets.length > MAX_IDENTITY_SET_SIZE)
396
+ throw new SquareError('invalid_args', `Channel needs between 1 and ${MAX_IDENTITY_SET_SIZE} recipients.`);
397
+ const unique = [];
398
+ for (const target of targets) {
399
+ if (typeof target !== 'string')
400
+ throw new SquareError('invalid_args', 'Channel recipients must be names.');
401
+ validateName(target);
402
+ if (!unique.some(name => sameName(name, target)))
403
+ unique.push(target);
404
+ }
405
+ if (typeof body !== 'string' || body.trim() === '')
406
+ throw new SquareError('invalid_args', 'Channel body must be non-empty.');
407
+ if (truncate !== false && (!Number.isSafeInteger(truncate) || truncate < 1))
408
+ throw new SquareError('invalid_args', 'Channel truncate must be false or a positive integer.');
409
+ const fullBody = body.replace(/\r\n/g, '\n');
410
+ const preview = truncate !== false && fullBody.length > truncate
411
+ ? `${fullBody.slice(0, truncate).trimEnd()}… (truncated ${fullBody.length - truncate} chars)`
412
+ : undefined;
413
+ const delivery = preview === undefined ? { mode: 'direct' } : { mode: 'direct', preview };
414
+ const metadata = validateActivityMetadata(input.metadata);
415
+ const { stored, resolvedTo } = (await this.artifact.transactAs({ kind: 'send', actor: from }, facts => {
416
+ const standing = facts.roster().filter(p => p.joined), resolvedTo = [], missing = [];
417
+ for (const target of unique) {
418
+ const recipient = standing.find(p => sameName(p.name, target));
419
+ if (recipient === undefined || facts.seat(recipient.name)?.attachment !== 'attached')
420
+ missing.push(target);
421
+ else
422
+ resolvedTo.push(recipient.name);
423
+ }
424
+ if (missing.length > 0)
425
+ throw new SquareError('invalid_args', `Channel recipients are not standing: ${missing.join(', ')}`, {
426
+ reason: 'not_standing', targets: missing, activityUnsent: true,
427
+ });
428
+ return { stored: facts.land({ kind: 'say', actor: from, at: this.clock(), body: fullBody,
429
+ ...(activityMetadataHasContent(metadata) ? { metadata } : {}), delivery, mentions: resolvedTo }), resolvedTo };
430
+ }, control?.signal)).value;
431
+ if (this.wakeTransport !== undefined)
432
+ await deliverPending({ artifact: this.artifact, transport: this.wakeTransport, location: this.deliveryLocation,
433
+ activity: stored.index, now: stored.at }).catch(() => undefined);
434
+ return { id: formatActivityId(stored.index), at: stored.at, from: stored.actor, to: resolvedTo, body: preview ?? fullBody,
435
+ ...(stored.kind !== 'say' || stored.metadata === undefined ? {} : { metadata: stored.metadata }) };
436
+ }
385
437
  async participant(name, { owner }) {
386
438
  validateName(name);
387
439
  requireOwner(owner);
@@ -26,6 +26,7 @@ export type Act = {
26
26
  at?: number;
27
27
  body?: string;
28
28
  metadata?: import('./model.js').ActivityMetadata;
29
+ delivery?: import('./model.js').ActivityDelivery;
29
30
  mentions?: readonly Participant[];
30
31
  reach?: Reach;
31
32
  reply?: number;
@@ -1,7 +1,16 @@
1
- import { SquareError, formatActivityId, parseActivityId, sameName, validateOwner, validateName } from './model.js';
1
+ import { SquareError, formatActivityId, parseActivityId, sameName, validateActivityDelivery, validateOwner, validateName } from './model.js';
2
2
  import { resolveAudience } from './attention.js';
3
3
  /** The single landing transition, shared by both adapters and explicit rebuild. */
4
4
  export function deriveLanding(activity, facts) {
5
+ if (activity.kind === 'say' && activity.delivery?.mode === 'direct') {
6
+ const audience = [];
7
+ for (const mention of activity.mentions ?? []) {
8
+ const recipient = facts.standing.find(item => sameName(item.name, mention))?.name;
9
+ if (recipient !== undefined && !audience.some(item => sameName(item.recipient, recipient)))
10
+ audience.push({ recipient, route: 'mention' });
11
+ }
12
+ return { activity, audience };
13
+ }
5
14
  const audience = activity.kind === 'say' ? resolveAudience({
6
15
  actor: activity.actor, recipients: facts.standing.map((item) => item.name),
7
16
  mentions: activity.mentions, replyAuthor: facts.replyAuthor, reach: activity.reach,
@@ -60,15 +69,15 @@ export function wrapSquareWrite(read, writes, authority) {
60
69
  throw new SquareError('invalid_args', 'Artifact writes require an explicit authority.');
61
70
  const { applyLanding, ...mutations } = writes;
62
71
  const seated = authority.kind === 'participant' || authority.kind === 'host-cleanup';
63
- const expectedName = seated ? authority.seat?.participant : authority.participant;
64
- if (!['participant', 'host-cleanup', 'join', 'takeover'].includes(authority.kind) || typeof expectedName !== 'string')
72
+ const expectedName = seated ? authority.seat?.participant : authority.kind === 'send' ? authority.actor : authority.participant;
73
+ if (!['participant', 'host-cleanup', 'join', 'takeover', 'send'].includes(authority.kind) || typeof expectedName !== 'string')
65
74
  throw new SquareError('invalid_args', 'Artifact write authority is malformed.');
66
75
  validateName(expectedName);
67
76
  if (seated) {
68
77
  validateOwner(authority.seat.owner);
69
78
  validateGeneration(authority.seat.generation);
70
79
  }
71
- else {
80
+ else if (authority.kind !== 'send') {
72
81
  validateOwner(authority.owner);
73
82
  if (authority.kind === 'takeover')
74
83
  validateGeneration(authority.expectedGeneration);
@@ -100,7 +109,7 @@ export function wrapSquareWrite(read, writes, authority) {
100
109
  reconnectSeat = seat;
101
110
  }
102
111
  }
103
- else {
112
+ else if (authority.kind === 'takeover') {
104
113
  const current = read.seat(expectedName);
105
114
  if (current === undefined || current.generation !== authority.expectedGeneration)
106
115
  throw new SquareError('already_joined', 'The participant seat changed before takeover.');
@@ -114,7 +123,13 @@ export function wrapSquareWrite(read, writes, authority) {
114
123
  validateName(actor);
115
124
  if (!sameName(actor, expectedName))
116
125
  throw new SquareError('already_joined', 'Activity actor does not match the artifact authority.');
117
- if (authority.kind === 'participant') {
126
+ if (activity.kind === 'say')
127
+ validateActivityDelivery(activity.delivery);
128
+ if (authority.kind === 'send') {
129
+ if (activity.kind !== 'say' || activity.delivery?.mode !== 'direct' || activity.reach !== undefined || activity.reply !== undefined)
130
+ throw new SquareError('invalid_args', 'Send authority may only land direct messages without bell or reply.');
131
+ }
132
+ else if (authority.kind === 'participant') {
118
133
  assertSeat(authority.seat);
119
134
  if (activity.kind === 'join')
120
135
  throw new SquareError('invalid_args', 'A participant seat can only be re-established by join.');
@@ -150,7 +165,7 @@ export function wrapSquareWrite(read, writes, authority) {
150
165
  const stored = { ...activity, index };
151
166
  applyLanding(deriveLanding(stored, landingFacts(read, stored)));
152
167
  if (stored.kind === 'join') {
153
- if (seated)
168
+ if (authority.kind !== 'join' && authority.kind !== 'takeover')
154
169
  throw new SquareError('invalid_args', 'Seat authority cannot establish another seat.');
155
170
  const seat = {
156
171
  participant: stored.actor,
@@ -1,10 +1,10 @@
1
1
  import type { DeliveryDeferral, MetadataMatch } from './delivery-deferral.js';
2
2
  export type { DeliveryDeferral, MetadataMatch, MetadataScalar } from './delivery-deferral.js';
3
3
  import type { ActivityId, Perception, Reach } from './square-core.js';
4
- import type { Owner, StoredAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
4
+ import type { Owner, StoredAct, WakeAddress, ActivityMetadata, ActivityDelivery, DirectedNotificationRoute } from './model.js';
5
5
  import type { WakeTransportPort, SquareResourceOwner } from './ports.js';
6
6
  export type { ActivityId, Perception, Reach } from './square-core.js';
7
- export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
7
+ export type { Owner, WakeAddress, ActivityMetadata, ActivityDelivery, JsonValue, DirectedNotificationRoute } from './model.js';
8
8
  /** Indexed archive event, including read bookkeeping. */
9
9
  export type ArchiveRecord = Readonly<StoredAct>;
10
10
  export interface Activity {
@@ -14,6 +14,7 @@ export interface Activity {
14
14
  readonly actor: string;
15
15
  readonly body?: string;
16
16
  readonly metadata?: ActivityMetadata;
17
+ readonly delivery?: ActivityDelivery;
17
18
  readonly mentions: readonly string[];
18
19
  readonly reach?: Reach;
19
20
  readonly target?: string;
@@ -49,6 +50,22 @@ export interface ExpressResult {
49
50
  readonly hint?: string;
50
51
  readonly observation?: ExpressObservation;
51
52
  }
53
+ export type ChannelTruncate = number | false;
54
+ export interface ChannelSendInput {
55
+ readonly from: string;
56
+ readonly to: string | readonly string[];
57
+ readonly body: string;
58
+ readonly truncate?: ChannelTruncate;
59
+ readonly metadata?: ActivityMetadata;
60
+ }
61
+ export interface ChannelSendResult {
62
+ readonly id: ActivityId;
63
+ readonly at: number;
64
+ readonly from: string;
65
+ readonly to: readonly string[];
66
+ readonly body: string;
67
+ readonly metadata?: ActivityMetadata;
68
+ }
52
69
  export interface ListenerChangeResult {
53
70
  readonly activity: Activity | null;
54
71
  readonly observation?: ListenerObservation;
@@ -170,6 +187,7 @@ export interface JoinResult {
170
187
  }
171
188
  export interface Participant {
172
189
  readonly name: string;
190
+ readonly generation: ActivityId;
173
191
  express(body: string | undefined, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
174
192
  express(input: ExpressInput, control?: OperationControl): Promise<ExpressResult>;
175
193
  listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync as MemoryDatabase } from 'node:sqlite';
2
- import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, activityMetadataHasContent } from './model.js';
2
+ import { SquareError, nameKey, isWakeRouteKind, validateOwner, validateActivityMetadata, validateActivityDelivery, activityMetadataHasContent } from './model.js';
3
3
  import { validateMetadataMatch, matchingDeferrals, validateDeferralId } from './delivery-deferral.js';
4
4
  import { deriveLanding, landingFacts } from './square-domain.js';
5
5
  import { claimAttempt, prepareAttempt, settleAttempt } from './delivery-attempts.js';
@@ -269,7 +269,16 @@ export function validateStoredActivity(value) {
269
269
  string(a.actor, true);
270
270
  switch (a.kind) {
271
271
  case 'say':
272
- shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata']);
272
+ shape(a, [...common], ['body', 'mentions', 'reach', 'reply', 'metadata', 'delivery']);
273
+ if (Object.hasOwn(a, 'delivery')) {
274
+ requireFact(a.delivery !== undefined, 'say delivery is malformed.');
275
+ try {
276
+ validateActivityDelivery(a.delivery);
277
+ }
278
+ catch {
279
+ throw invalidArtifact('say delivery is malformed.');
280
+ }
281
+ }
273
282
  if (Object.hasOwn(a, 'body'))
274
283
  string(a.body);
275
284
  if (Object.hasOwn(a, 'metadata')) {
@@ -311,7 +320,7 @@ export function validateStoredActivity(value) {
311
320
  function activity(row) {
312
321
  const h = head(row), payload = object(json(row.payload_json));
313
322
  // Payload cannot shadow indexed structural columns.
314
- const allowed = h.kind === 'say' ? ['body', 'mentions', 'metadata'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
323
+ const allowed = h.kind === 'say' ? ['body', 'mentions', 'metadata', 'delivery'] : h.kind === 'done' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
315
324
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
316
325
  return immutable(validateStoredActivity({ ...h, ...payload }));
317
326
  }
@@ -13,6 +13,7 @@ interface ToolDefinition {
13
13
  /** Human text identical to what the CLI prints; domain receipts stay inside the adapter. */
14
14
  run(square: Square, args: JsonObject, control: OperationControl, identity: ToolIdentity): Promise<string>;
15
15
  }
16
+ export declare function squareToolsEnabled(env?: NodeJS.ProcessEnv): boolean;
16
17
  export declare const SQUARE_TOOL_INSTRUCTIONS = "Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.";
17
18
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
18
19
  export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod/v4';
2
- import { isSquareError } from './model.js';
2
+ import { isSquareError, SquareError } from './model.js';
3
3
  import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
4
4
  import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
5
5
  const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
@@ -19,6 +19,14 @@ function tool(name, description, shape, run, render) {
19
19
  return render(receipt, operationArgs) ?? '';
20
20
  } };
21
21
  }
22
+ export function squareToolsEnabled(env = process.env) {
23
+ const raw = env.SQUARE_TOOLS?.trim().toLowerCase();
24
+ if (raw === undefined || raw === '' || raw === '1' || raw === 'true')
25
+ return true;
26
+ if (raw === '0' || raw === 'false' || raw === 'none')
27
+ return false;
28
+ throw new SquareError('invalid_args', 'Invalid SQUARE_TOOLS: expected 0, 1, true, false, or none.');
29
+ }
22
30
  export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
23
31
  const target = z.string().describe('The participant to turn toward or away from.');
24
32
  export const SQUARE_TOOLS = [
package/dist/views.js CHANGED
@@ -19,6 +19,7 @@ export function toPublicActivity(stored) {
19
19
  ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
20
20
  ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
21
21
  ...(stored.kind === 'say' && stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
22
+ ...(stored.kind === 'say' && stored.delivery !== undefined ? { delivery: stored.delivery } : {}),
22
23
  mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}),
23
24
  ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
24
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.95",
3
+ "version": "0.3.96",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,6 +51,10 @@
51
51
  "ws": "^8.21.3"
52
52
  },
53
53
  "exports": {
54
+ "./channel": {
55
+ "types": "./dist/channel.d.ts",
56
+ "default": "./dist/channel.js"
57
+ },
54
58
  "./delivery": {
55
59
  "types": "./dist/delivery-primitives.d.ts",
56
60
  "default": "./dist/delivery-primitives.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.95",
3
+ "version": "0.3.96",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.95",
3
+ "version": "0.3.96",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -0,0 +1,7 @@
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=";
@@ -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
  }