@astrosheep/square 0.3.57 → 0.3.61

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/list.js CHANGED
@@ -5,6 +5,7 @@ import { closeOpenSquare } from './open-square.js';
5
5
  import { listPresentation } from './views.js';
6
6
  import { formatRelativeTime } from './time.js';
7
7
  import { participantIdentity, quoteShell } from './presentation.js';
8
+ import { style } from './tty-style.js';
8
9
  const DEFAULT_LIST_DEPTH = 4;
9
10
  const MAX_LIST_DEPTH = 16;
10
11
  const DEFAULT_LIST_LIMIT = 20;
@@ -84,35 +85,37 @@ function renderSquareList(items, stopped, depth, limit, after) {
84
85
  const page = after === undefined ? items : items.filter((item) => item.path.localeCompare(after) > 0);
85
86
  const shownItems = page.slice(0, limit);
86
87
  if (items.length === 0) {
87
- return [`(no squares found)`, ...(stopped ? [`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`] : [])].join('\n') + '\n';
88
+ return [`(no squares found)`, ...(stopped ? [style('dim', `○ the walk stopped early — deeper squares may still be hiding`)] : [])].join('\n') + '\n';
88
89
  }
89
90
  const now = Date.now();
90
91
  const lines = ['squares'];
91
92
  for (const item of shownItems) {
92
- lines.push(`${item.activities > 0 ? '●' : '○'} ${characterPreview(item.path)} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in square · ${item.activities} activities`);
93
+ const glyph = item.activities > 0 ? '●' : style('dim', '○');
94
+ lines.push(`${glyph} ${characterPreview(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
93
95
  const shownContext = item.context.slice(0, CONTEXT_PREVIEW_LINES);
94
96
  if (shownContext.length === 0) {
95
- lines.push(' context · (none)');
97
+ lines.push(` ${style('dim', 'context')} · (none)`);
96
98
  }
97
99
  else {
98
- shownContext.forEach((line, index) => lines.push(` ${index === 0 ? 'context' : ' '} · ${characterPreview(line)}`));
100
+ shownContext.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${characterPreview(line)}`));
99
101
  const hiddenContext = item.context.length - shownContext.length;
100
102
  if (hiddenContext > 0)
101
- lines.push(` · … ${hiddenContext} more ${hiddenContext === 1 ? 'line' : 'lines'}`);
103
+ lines.push(style('dim', ` · … ${hiddenContext} more ${hiddenContext === 1 ? 'line' : 'lines'}`));
102
104
  }
103
105
  const shownParticipants = item.participants.slice(0, PARTICIPANT_PREVIEW_COUNT);
104
106
  const hiddenParticipants = item.participants.length - shownParticipants.length;
105
- lines.push(` participants · ${shownParticipants.length === 0 ? 'nobody' : shownParticipants.map((participant) => participantIdentity(characterPreview(participant))).join(' · ')}${hiddenParticipants > 0 ? ` · … ${hiddenParticipants} more` : ''}`);
107
+ const participantNames = shownParticipants.length === 0 ? 'nobody' : shownParticipants.map((participant) => participantIdentity(characterPreview(participant))).join(' · ');
108
+ lines.push(` ${style('dim', 'participants')} · ${participantNames}${hiddenParticipants > 0 ? style('dim', ` · … ${hiddenParticipants} more`) : ''}`);
106
109
  }
107
110
  const hiddenItems = page.length - shownItems.length;
108
111
  if (hiddenItems > 0) {
109
112
  const cursor = shownItems.at(-1)?.path;
110
- lines.push(stopped ? ' … more squares' : ` … ${hiddenItems} more ${hiddenItems === 1 ? 'square' : 'squares'}`);
113
+ lines.push(style('dim', stopped ? ' … more squares' : ` … ${hiddenItems} more ${hiddenItems === 1 ? 'square' : 'squares'}`));
111
114
  if (cursor !== undefined)
112
- lines.push(`» square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
115
+ lines.push(`square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
113
116
  }
114
117
  if (stopped)
115
- lines.push(`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`);
118
+ lines.push(style('dim', `○ the walk stopped early — deeper squares may still be hiding`));
116
119
  return lines.join('\n') + '\n';
117
120
  }
118
121
  function parseListOptions(args, usage) {
@@ -102,28 +102,28 @@ export async function createDefaultWakeTransport(hostLedger, clock, env = proces
102
102
  const adapters = await defaultWakeAdapters();
103
103
  return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
104
104
  }
105
- async function wakeRequestIsCurrent(request, hostLedger, now) {
105
+ async function wakeRequestCurrentness(request, hostLedger, now) {
106
106
  const activity = parseActivityId(request.activity);
107
107
  if (activity === undefined)
108
- return false;
108
+ return { current: false, activityPending: false, sessionBound: false, routePublished: false, observationAvailable: true };
109
109
  let square;
110
110
  try {
111
111
  square = await openSquare(request.location, { hostLedger });
112
112
  const current = await observeSquare({ artifact: square.artifact, hostLedger, location: request.location, now });
113
- const pending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(request.participant)
113
+ const activityPending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(request.participant)
114
114
  && entry.notifications.some((notification) => notification.item.index === activity));
115
- const bound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(request.route.participant)
115
+ const sessionBound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(request.route.participant)
116
116
  && binding.sessionId === request.route.sessionId
117
117
  && binding.location === request.route.location);
118
- const published = (current.state.routes ?? []).some((route) => route.location === request.route.location
118
+ const routePublished = (current.state.routes ?? []).some((route) => route.location === request.route.location
119
119
  && nameKey(route.participant) === nameKey(request.route.participant)
120
120
  && route.sessionId === request.route.sessionId
121
121
  && route.kind === request.route.kind
122
122
  && JSON.stringify(route.address) === JSON.stringify(request.route.address));
123
- return pending && bound && published;
123
+ return { current: activityPending && sessionBound && routePublished, activityPending, sessionBound, routePublished, observationAvailable: true };
124
124
  }
125
125
  catch {
126
- return false;
126
+ return { current: false, activityPending: false, sessionBound: false, routePublished: false, observationAvailable: false };
127
127
  }
128
128
  finally {
129
129
  if (square !== undefined)
@@ -151,15 +151,32 @@ export function createWakeTransport(adapters, hostLedger, clock) {
151
151
  if (adapter === undefined)
152
152
  return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
153
153
  try {
154
- const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => (await (beforeSend ?? (async () => true))()) && await wakeRequestIsCurrent(request, hostLedger, clock()), timeoutMs);
154
+ let revalidation;
155
+ const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => {
156
+ if (!(await (beforeSend ?? (async () => true))()))
157
+ return false;
158
+ revalidation = await wakeRequestCurrentness(request, hostLedger, clock());
159
+ return revalidation.current;
160
+ }, timeoutMs);
155
161
  if (result.outcome === 'accepted')
156
162
  return { outcome: 'accepted' };
157
163
  if (result.outcome === 'failed')
158
- return { outcome: 'failed', message: result.message };
164
+ return { outcome: 'failed', ...(result.signature === undefined ? {} : { signature: result.signature }), message: result.message, ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }) };
159
165
  if (result.outcome === 'unavailable')
160
- return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
166
+ return { outcome: 'failed', signature: result.signature, message: result.message, ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }), unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
161
167
  if (result.outcome === 'unknown')
162
- return { outcome: 'unknown', diagnostic: result.message };
168
+ return { outcome: 'unknown', ...(result.signature === undefined ? {} : { signature: result.signature }), ...(result.message === undefined ? {} : { message: result.message }), ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }) };
169
+ if (result.outcome === 'cancelled' && revalidation !== undefined && !revalidation.current) {
170
+ return {
171
+ outcome: 'failed',
172
+ signature: 'pre_send_revalidation_failed',
173
+ message: revalidation.observationAvailable
174
+ ? 'Wake was not sent because current attention, session binding, or route no longer matches.'
175
+ : 'Wake was not sent because current Square delivery state could not be verified.',
176
+ diagnostic: revalidation,
177
+ unavailable: true,
178
+ };
179
+ }
163
180
  return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
164
181
  }
165
182
  catch (error) {
package/dist/ports.d.ts CHANGED
@@ -69,6 +69,8 @@ export type WakeOutcome = {
69
69
  } | {
70
70
  readonly outcome: 'failed';
71
71
  readonly message?: string;
72
+ readonly signature?: string;
73
+ readonly diagnostic?: unknown;
72
74
  readonly attemptN?: number;
73
75
  readonly unavailable?: boolean;
74
76
  readonly retainRoute?: boolean;
@@ -78,7 +80,9 @@ export type WakeOutcome = {
78
80
  readonly diagnostic?: string;
79
81
  } | {
80
82
  readonly outcome: 'unknown';
81
- readonly diagnostic?: string;
83
+ readonly signature?: string;
84
+ readonly message?: string;
85
+ readonly diagnostic?: unknown;
82
86
  readonly attemptN?: number;
83
87
  };
84
88
  export interface SquareObservation {
@@ -6,6 +6,8 @@ export type WatchStatus = 'stale' | 'empty-now' | 'quorum' | 'capped';
6
6
  interface HeaderOptions {
7
7
  participantCount?: number;
8
8
  held?: boolean;
9
+ /** Refusal bodies go to stderr, which stays literal even when it is a TTY. */
10
+ plain?: boolean;
9
11
  }
10
12
  interface ParticipantOutputOptions {
11
13
  squarePath: string;
@@ -46,6 +48,11 @@ interface WatchStatusOptions extends ParticipantOutputOptions {
46
48
  }
47
49
  export declare function displayPath(squarePath: string, cwd?: string): string;
48
50
  export declare function withPathOutput(squarePath: string, body?: string, opts?: HeaderOptions): string;
51
+ /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
52
+ export declare function formatRefusal(squarePath: string, bodyLines: string[], opts?: {
53
+ participantCount?: number;
54
+ held?: boolean;
55
+ }): string;
49
56
  export declare function quoteShell(value: string): string;
50
57
  export declare function commandPrefix(squarePath: string): string;
51
58
  export declare function participantCommandPrefix(squarePath: string, name: string): string;
@@ -3,16 +3,18 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
- import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-core.js';
6
+ import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE, replayLandedAudiences } from './square-core.js';
7
7
  import { perceiveActivity } from './delivery.js';
8
- import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
8
+ import { actId, presenceAnchor, publicActs, rosterNames, sayNumberFor } from './runtime.js';
9
9
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
10
10
  import { compareParticipantActivity } from './decisions.js';
11
11
  import { grepSnippet } from './search.js';
12
+ import { style } from './tty-style.js';
12
13
  function headerLine(squarePath, opts = {}) {
13
14
  const count = opts.participantCount ?? 0;
14
15
  const heldSuffix = opts.held ? ' — a hand is raised' : '';
15
- return `· the square at ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
16
+ const line = `· ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
17
+ return opts.plain === true ? line : style('dim', line);
16
18
  }
17
19
  export function displayPath(squarePath, cwd = process.cwd()) {
18
20
  if (!path.isAbsolute(squarePath))
@@ -32,6 +34,14 @@ export function displayPath(squarePath, cwd = process.cwd()) {
32
34
  export function withPathOutput(squarePath, body = '', opts = {}) {
33
35
  return [headerLine(squarePath, opts), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
34
36
  }
37
+ /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
38
+ export function formatRefusal(squarePath, bodyLines, opts = {}) {
39
+ const body = bodyLines.join('\n');
40
+ // A refusal with no known count stays bare rather than claiming "0 in the square".
41
+ if (opts.participantCount === undefined)
42
+ return body === '' ? '' : `${body}\n`;
43
+ return withPathOutput(squarePath, body, { ...opts, plain: true });
44
+ }
35
45
  export function quoteShell(value) {
36
46
  return `'${value.replace(/'/g, `'\\''`)}'`;
37
47
  }
@@ -43,10 +53,10 @@ export function participantCommandPrefix(squarePath, name) {
43
53
  }
44
54
  /** Blocked actions end with a full copy-pasteable recovery command. */
45
55
  export function joinRecoveryCommand(squarePath, name) {
46
- return `» ${participantCommandPrefix(squarePath, name)} join`;
56
+ return `${participantCommandPrefix(squarePath, name)} join`;
47
57
  }
48
58
  export function participantsRecoveryCommand(squarePath) {
49
- return `» ${commandPrefix(squarePath)} participants`;
59
+ return `${commandPrefix(squarePath)} participants`;
50
60
  }
51
61
  function formatAge(ms) {
52
62
  if (ms === undefined)
@@ -68,6 +78,10 @@ function presenceGlyph(participant) {
68
78
  return '●';
69
79
  return '○';
70
80
  }
81
+ function styledPresenceGlyph(participant) {
82
+ const glyph = presenceGlyph(participant);
83
+ return glyph === '○' ? style('dim', glyph) : glyph;
84
+ }
71
85
  function presenceText(participant, now) {
72
86
  if (participant.state === 'done') {
73
87
  return participant.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(participant.lastActiveAt, now)}`;
@@ -86,17 +100,17 @@ export function renderPresenceLines(participants, now, limit = 5) {
86
100
  .sort(compareParticipantActivity);
87
101
  const shown = recent.slice(0, limit);
88
102
  if (shown.length === 0)
89
- return [' ○ nobody nearby'];
90
- const lines = shown.map((p) => ` ${presenceGlyph(p)} ${participantIdentity(p.name)} · ${presenceText(p, now)}`);
103
+ return [style('dim', ' ○ nobody nearby')];
104
+ const lines = shown.map((p) => ` ${styledPresenceGlyph(p)} ${participantIdentity(p.name)}${style('dim', ` · ${presenceText(p, now)}`)}`);
91
105
  const remaining = recent.length - shown.length;
92
106
  if (remaining > 0)
93
- lines.push(` ○ …and ${remaining} more`);
107
+ lines.push(style('dim', ` ○ … ${remaining} more nearby`));
94
108
  return lines;
95
109
  }
96
110
  const EXPRESS_HINTS = [
97
111
  '*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
98
112
  "you're standing in a square — words and gestures both land",
99
- 'the square is for conversation — share thoughts and engage with others, beyond status updates and empty acknowledgments',
113
+ 'the square runs on conversation — say things that move it, not nods that fill it',
100
114
  ];
101
115
  export function expressHintLine(ownActivityCount) {
102
116
  if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
@@ -119,7 +133,7 @@ export function truncateExternalDiagnostic(diagnostic) {
119
133
  }
120
134
  function previewBody(body, maxLen = BODY_PREVIEW_LENGTH) {
121
135
  const preview = truncateChars(body, maxLen);
122
- return preview.remaining === 0 ? preview.text : `${preview.text}\n… ${preview.remaining} more chars`;
136
+ return preview.remaining === 0 ? preview.text : `${preview.text}\n${style('dim', `… ${preview.remaining} more chars`)}`;
123
137
  }
124
138
  const UNREAD_PREVIEW_CHARS = 120;
125
139
  export function previewActivityBody(body) {
@@ -127,7 +141,7 @@ export function previewActivityBody(body) {
127
141
  if (compact === '')
128
142
  return '(empty)';
129
143
  const preview = truncateChars(compact, UNREAD_PREVIEW_CHARS);
130
- return preview.remaining === 0 ? preview.text : `${preview.text}… (+${preview.remaining} chars)`;
144
+ return preview.remaining === 0 ? preview.text : `${preview.text}${style('dim', `… (+${preview.remaining} chars)`)}`;
131
145
  }
132
146
  export function renderRoomChangeText(event) {
133
147
  const actor = event.actor ?? 'someone';
@@ -166,7 +180,7 @@ export function renderEventCli(event, opts = {}) {
166
180
  case 'hold':
167
181
  return `· ${renderRoomChangeText(event)}`;
168
182
  case 'resume':
169
- return `✓ ${renderRoomChangeText(event)}`;
183
+ return `${style('release', '✓')} ${renderRoomChangeText(event)}`;
170
184
  case 'listen':
171
185
  case 'ignore':
172
186
  return `· ${renderRoomChangeText(event)}`;
@@ -177,11 +191,12 @@ export function renderEventCli(event, opts = {}) {
177
191
  ? ` · calls your name across the square — ${participantIdentity(mention)}`
178
192
  : '';
179
193
  const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
180
- return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}${bodySuffix(body)}`;
194
+ const meta = ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
195
+ return `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body)}`;
181
196
  }
182
197
  case 'done': {
183
198
  const body = renderedBody(event.body, maxBody);
184
- return `○ ${participantIdentity(event.actor)} stepped out of the square — done · ${actId(event)} · ${formatRelativeTime(event.at, now)}${bodySuffix(body)}`;
199
+ return `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${actId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body)}`;
185
200
  }
186
201
  case 'read':
187
202
  return '';
@@ -190,8 +205,9 @@ export function renderEventCli(event, opts = {}) {
190
205
  function renderPresenceOnlySay(event, opts = {}) {
191
206
  const audience = audienceOf(event);
192
207
  const targets = audience.kind === 'bell' ? [] : audience.names;
208
+ const meta = style('dim', ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
193
209
  if (targets.length === 0) {
194
- return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`;
210
+ return `● ${participantIdentity(event.actor)}${meta}`;
195
211
  }
196
212
  const visibleTargets = targets.slice(0, MAX_IDENTITY_SET_SIZE).map((name) => participantIdentity(name));
197
213
  const remainingTargets = targets.length - visibleTargets.length;
@@ -199,7 +215,7 @@ function renderPresenceOnlySay(event, opts = {}) {
199
215
  ...visibleTargets,
200
216
  ...(remainingTargets === 0 ? [] : [`${remainingTargets} ${remainingTargets === 1 ? 'other' : 'others'}`]),
201
217
  ].join(' and ')}`;
202
- return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}\n talked to${dest}`;
218
+ return `● ${participantIdentity(event.actor)}${meta}\n${style('dim', ` talked to${dest}`)}`;
203
219
  }
204
220
  export function renderAmbientEvent(event, viewer, opts = {}) {
205
221
  if (event.kind !== 'say')
@@ -216,7 +232,7 @@ export function renderAmbientEvent(event, viewer, opts = {}) {
216
232
  return renderEventCli(event, opts);
217
233
  }
218
234
  function draftSavedLines(draftPath) {
219
- return draftPath === undefined ? [] : [`· draft kept: ${draftPath}`];
235
+ return draftPath === undefined ? [] : [style('dim', `· draft kept: ${draftPath}`)];
220
236
  }
221
237
  function withDraftInput(command, draftPath) {
222
238
  return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
@@ -229,15 +245,15 @@ function renderUnreadSummary(opts) {
229
245
  ...item.previews.slice(-1).map((preview) => {
230
246
  const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number, perception: preview.perception });
231
247
  if (rendered === '')
232
- return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
248
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`);
233
249
  if (preview.perception === 'presence') {
234
- return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`;
250
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`);
235
251
  }
236
- return ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
252
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`);
237
253
  }),
238
254
  ]),
239
- ...(remainingSummaries === 0 ? [] : [` · ${remainingSummaries} more participants have unread activity`]),
240
- ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
255
+ ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
256
+ ...opts.roomChanges.map((act) => style('dim', ` · ${renderRoomChangeText(act)}`)),
241
257
  ];
242
258
  }
243
259
  export function renderPendingFeed(history, publicItems, roomChanges, viewer = '', squareState) {
@@ -261,12 +277,12 @@ export function renderPendingFeed(history, publicItems, roomChanges, viewer = ''
261
277
  export function renderActivityBlocked(opts) {
262
278
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch --now`;
263
279
  return withPathOutput(opts.squarePath, [
264
- "✕ your activity doesn't land — the square moved behind your back",
280
+ `${style('blocked', '✕')} your activity doesn't land — the square moved behind your back`,
265
281
  ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, roomChanges: opts.unreadRoomChanges, viewer: opts.name }),
266
282
  ...draftSavedLines(opts.draftPath),
267
- `» ${readNowCommand}`,
283
+ `${readNowCommand}`,
268
284
  ' take it in, then express again',
269
- `» ${withDraftInput(opts.forceCommand, opts.draftPath)}`,
285
+ `${withDraftInput(opts.forceCommand, opts.draftPath)}`,
270
286
  ' only if you truly mean to express over unread activity',
271
287
  ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
272
288
  }
@@ -280,16 +296,16 @@ export function renderExpressNoWait(opts) {
280
296
  const retryCommand = opts.forceCommand;
281
297
  const lines = opts.reason === 'throttled'
282
298
  ? [
283
- '✕ the square is packed',
299
+ `${style('blocked', '✕')} the square is packed`,
284
300
  ` · next opening in ${formatDuration(opts.delayMs)}`,
285
301
  ...draftSavedLines(opts.draftPath),
286
- `» ${withDraftInput(retryCommand, opts.draftPath)}`,
302
+ `${withDraftInput(retryCommand, opts.draftPath)}`,
287
303
  ]
288
304
  : [
289
- "✕ your activity doesn't land — a hand is raised",
290
- ` · ${opts.holdReason ?? 'the square holds its breath'}`,
305
+ `${style('blocked', '✕')} your activity doesn't land — a hand is raised`,
306
+ style('dim', ` · ${opts.holdReason ?? 'the square holds its breath'}`),
291
307
  ...draftSavedLines(opts.draftPath),
292
- `» ${withDraftInput(retryCommand, opts.draftPath)}`,
308
+ `${withDraftInput(retryCommand, opts.draftPath)}`,
293
309
  ];
294
310
  return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount, held: opts.held });
295
311
  }
@@ -302,32 +318,24 @@ export function renderPublicTail(squareState, events, lastN, now, viewer = '') {
302
318
  .filter(Boolean)
303
319
  .join('\n\n');
304
320
  }
305
- function lastPresenceAnchor(squareState, name) {
306
- const cursor = readCursor(squareState, name);
307
- for (let i = squareState.acts.length - 1; i >= 0; i--) {
308
- const event = squareState.acts[i];
309
- const index = event.index;
310
- if (index > cursor)
311
- continue;
312
- if (event.kind === 'say' || event.kind === 'done')
313
- return index;
314
- }
315
- return -1;
321
+ function lastPresenceAnchor(squareState, name, landed) {
322
+ return presenceAnchor(squareState, name, landed);
316
323
  }
317
324
  export function renderPresenceAnchor(names) {
318
325
  const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
319
326
  const participants = visibleNames.map((name) => participantIdentity(name)).join(', ');
320
327
  const remaining = names.length - visibleNames.length;
321
328
  const suffix = remaining === 0 ? '' : ` and ${remaining} more`;
322
- return names.length === 1 ? `→ ${participants} was here` : `→ ${participants}${suffix} were here`;
329
+ return style('dim', names.length === 1 ? `→ ${participants} was here` : `→ ${participants}${suffix} were here`);
323
330
  }
324
331
  export function renderActivitiesView(squareState, visible, lastN, noTruncate, squarePath, viewer = '', mode = 'ambient') {
325
332
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
326
333
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
327
334
  const previewLen = noTruncate ? undefined : BODY_PREVIEW_LENGTH;
328
335
  const markers = new Map();
336
+ const landed = replayLandedAudiences(squareState.acts);
329
337
  for (const participant of rosterNames(squareState)) {
330
- const anchor = lastPresenceAnchor(squareState, participant);
338
+ const anchor = lastPresenceAnchor(squareState, participant, landed);
331
339
  if (anchor >= 0)
332
340
  markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
333
341
  }
@@ -351,15 +359,13 @@ export function renderActivitiesView(squareState, visible, lastN, noTruncate, sq
351
359
  if (previewLen !== undefined) {
352
360
  const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceiveActivity(squareState, act, viewer) === 'full'));
353
361
  if (truncated)
354
- chunks.push(`» ${commandPrefix(squarePath)} history --no-truncate`);
362
+ chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
355
363
  }
356
364
  return chunks.join('\n\n');
357
365
  }
358
366
  const GREP_PREVIEW_CHARS = 160;
359
367
  function highlightGrepMatch(text) {
360
- if (!process.stdout.isTTY || process.env.NO_COLOR !== undefined || text === '')
361
- return text;
362
- return `\x1b[38;5;222m\x1b[1m${text}\x1b[0m`;
368
+ return style('match', text);
363
369
  }
364
370
  export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squarePath, pattern, fixed = false, perception) {
365
371
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
@@ -370,19 +376,20 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
370
376
  let truncated = false;
371
377
  for (const act of publicVisible) {
372
378
  const rawBody = act.body ?? '';
379
+ const header = `${style('dim', actId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
373
380
  if (perception?.(act) === 'presence') {
374
- chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}`);
381
+ chunks.push(header);
375
382
  continue;
376
383
  }
377
384
  if (noTruncate === true) {
378
385
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
379
- chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n${body}`);
386
+ chunks.push(`${header}\n${body}`);
380
387
  continue;
381
388
  }
382
389
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
383
390
  if (snippet === undefined) {
384
391
  const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
385
- chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}${preview === '' ? '' : `\n ${preview}`}`);
392
+ chunks.push(`${header}${preview === '' ? '' : `\n ${preview}`}`);
386
393
  continue;
387
394
  }
388
395
  const clippedBefore = snippet.beforeOmitted > 0;
@@ -390,15 +397,15 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
390
397
  truncated ||= clippedBefore || clippedAfter;
391
398
  const text = `${clippedBefore ? '… ' : ''}${snippet.before}${highlightGrepMatch(snippet.match)}${snippet.after}${clippedAfter ? ' …' : ''}`;
392
399
  const omitted = clippedBefore || clippedAfter
393
- ? `\n · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`
400
+ ? `\n${style('dim', ` · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`)}`
394
401
  : '';
395
- chunks.push(`${actId(act.index)} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
402
+ chunks.push(`${header}\n ${text.trim()}${omitted}`);
396
403
  }
397
404
  if (publicVisible.length === 1) {
398
- chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
405
+ chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
399
406
  }
400
407
  else if (truncated && publicVisible.length > 1) {
401
- chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --no-truncate`);
408
+ chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --no-truncate`);
402
409
  }
403
410
  return chunks.join('\n\n');
404
411
  }
@@ -406,9 +413,10 @@ function renderActivityLimitBody(opts) {
406
413
  const countText = opts.count !== undefined && opts.hardCap !== undefined ? ` (${opts.count}/${opts.hardCap})` : '';
407
414
  const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done -`;
408
415
  return [
409
- `✕ your activity doesn't land — the cap is reached${countText}`,
416
+ `${style('blocked', '✕')} your activity doesn't land — the cap is reached${countText}`,
410
417
  ...draftSavedLines(opts.draftPath),
411
- `» ${withDraftInput(doneCommand, opts.draftPath)}`,
418
+ `${withDraftInput(doneCommand, opts.draftPath)}`,
419
+ ' your draft becomes your final note',
412
420
  ].join('\n');
413
421
  }
414
422
  export function renderActivityLimit(opts) {
@@ -416,17 +424,17 @@ export function renderActivityLimit(opts) {
416
424
  }
417
425
  export function renderWatchAlreadyActive(opts) {
418
426
  return [
419
- '✕ you are already catching',
427
+ `${style('blocked', '✕')} you are already catching`,
420
428
  ` · an active catch is already running for ${participantIdentity(opts.name)}`,
421
- ' · wait for it to finish before starting another catch',
422
- `» ${commandPrefix(opts.squarePath)} participants`,
429
+ ' · --replace lets a new catch take over',
430
+ `${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`,
423
431
  ].join('\n');
424
432
  }
425
433
  export function renderWatchForceTakeover(_opts) {
426
434
  return '✓ your new catch takes over';
427
435
  }
428
436
  export function renderWatchReplaceMissing(_opts) {
429
- return '⚠ nothing to replace — no active catch was running; your catch started normally';
437
+ return '· no catch stood here — yours takes the spot';
430
438
  }
431
439
  export function renderWatchReplaced(_opts) {
432
440
  return '✕ a newer catch took over';
@@ -443,26 +451,26 @@ export function renderWatchStatus(opts) {
443
451
  case 'empty-now': {
444
452
  const prefix = participantCommandPrefix(opts.squarePath, opts.name);
445
453
  const quiet = opts.status === 'stale' && opts.idleMs !== undefined
446
- ? `○ ${formatDuration(opts.idleMs)} of quiet — nothing new for you`
447
- : '○ only footsteps in the square — nothing new for you';
454
+ ? style('dim', `○ ${formatDuration(opts.idleMs)} of quiet — nothing new for you`)
455
+ : style('dim', '○ only footsteps in the square — nothing new for you');
448
456
  return [
449
457
  quiet,
450
458
  ...(opts.showCatchHint === false
451
459
  ? []
452
- : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
460
+ : [`${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
453
461
  ...presenceLines,
454
462
  ].join('\n');
455
463
  }
456
464
  case 'quorum':
457
- return ['✓ everyone else has left — the square is yours alone', `» ${participantCommandPrefix(opts.squarePath, opts.name)} done -`].join('\n');
465
+ return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name)} done -`].join('\n');
458
466
  case 'capped':
459
- return ['✕ nothing left in you — the cap is reached', `» ${participantCommandPrefix(opts.squarePath, opts.name)} done -`].join('\n');
467
+ return [`${style('blocked', '✕')} nothing left in you — the cap is reached`, `${participantCommandPrefix(opts.squarePath, opts.name)} done -`].join('\n');
460
468
  }
461
469
  }
462
470
  function renderRoomChanges(changes) {
463
471
  if (changes.length === 0)
464
472
  return '';
465
- return ['▲ while your back was turned', ...changes.map((act) => ` · ${renderRoomChangeText(act)}`)].join('\n');
473
+ return [`${style('changed', '▲')} while your back was turned`, ...changes.map((act) => ` · ${renderRoomChangeText(act)}`)].join('\n');
466
474
  }
467
475
  export function renderDoctorClean() {
468
476
  return '✓ no problems found';
@@ -481,7 +489,7 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
481
489
  quiet,
482
490
  ...(opts.showCatchHint === false
483
491
  ? []
484
- : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
492
+ : [`${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
485
493
  ].join('\n'));
486
494
  }
487
495
  const publicIndexes = new Set(publicItems.map((item) => item.index));
package/dist/registry.js CHANGED
@@ -101,7 +101,7 @@ export async function claimSessionParticipant(squarePath, name, env = process.en
101
101
  epoch: 1,
102
102
  }, 'local');
103
103
  if (result.status === 'busy')
104
- throw new SquareError('already_joined', `${name} is already bound to another session`);
104
+ throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
105
105
  if (result.status === 'degraded')
106
106
  throw result.error;
107
107
  const epoch = presenceEpoch(result.record) || (result.status === 'acquired' ? 1 : 0);
@@ -203,7 +203,7 @@ export async function releaseSessionParticipant(squarePath, name, env = process.
203
203
  export async function bindCurrentParticipant(squarePath, name, env = process.env) { if (squareAssignedParticipantName(env) !== name)
204
204
  throw new SquareError('invalid_args', `The current session is not assigned ${name}`); const sessionId = await localParticipantOwner(squarePath, name, env); if (sessionId !== undefined)
205
205
  return { created: false, sessionId }; if ((await lookupParticipant(squarePath, name, Date.now(), env)).at(0) !== undefined)
206
- throw new SquareError('already_joined', `${name} is already bound to another session`); await recordLocalJoin(name, squarePath, env); const currentSessionId = await localParticipantOwner(squarePath, name, env); if (currentSessionId === undefined)
206
+ throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`); await recordLocalJoin(name, squarePath, env); const currentSessionId = await localParticipantOwner(squarePath, name, env); if (currentSessionId === undefined)
207
207
  throw new Error(`Current participant binding did not commit for ${name}`); return { created: true, sessionId: currentSessionId }; }
208
208
  export async function unbindCurrentParticipant(squarePath, name, env = process.env) { const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, Date.now(), env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
209
209
  await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel, env); return current.length > 0; }
package/dist/runtime.d.ts CHANGED
@@ -38,6 +38,14 @@ export declare function observationFor(squareState: SquareState, name: string, i
38
38
  export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number): boolean;
39
39
  export declare function readCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
40
40
  export declare function latestActIndex(acts: StoredAct[]): number;
41
+ /**
42
+ * "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
43
+ * and seen receipts after their latest join. Anything else — in particular acts merely
44
+ * not addressed to them — says nothing about where they actually were.
45
+ */
46
+ export declare function presenceEvidenceCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
47
+ /** The last public act the participant was verifiably present for, or -1 when there is no evidence. */
48
+ export declare function presenceAnchor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
41
49
  export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
42
50
  export declare function watchLease(squareState: SquareState, name: string): WatchLease | undefined;
43
51
  export declare function writeWatchLease(squareState: SquareState, name: string, lease: WatchLease): void;