@astrosheep/square 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. package/dist/terminal.js +0 -125
@@ -1,206 +1,76 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import { setTimeout as sleep } from 'node:timers/promises';
3
- const DEFAULT_POLL_INTERVAL_MS = 100;
4
- const DEFAULT_REQUEST_TIMEOUT_MS = 3000;
5
- function paseoWebSocketUrl() {
6
- const override = process.env['SQUARE_PASEO_WS_URL']?.trim();
7
- if (override)
8
- return override;
9
- const listen = process.env['PASEO_LISTEN']?.trim();
10
- if (!listen)
11
- return 'ws://127.0.0.1:6767/ws';
12
- if (/^wss?:\/\//i.test(listen)) {
13
- const url = new URL(listen);
14
- if (url.pathname === '/' || url.pathname === '')
15
- url.pathname = '/ws';
16
- return url.toString();
17
- }
18
- if (/^tcp:\/\//i.test(listen)) {
19
- const url = new URL(listen);
20
- const secure = url.searchParams.get('ssl') === 'true';
21
- url.protocol = secure ? 'wss:' : 'ws:';
22
- url.pathname = '/ws';
23
- return url.toString();
24
- }
25
- if (/^\d+$/.test(listen))
26
- return `ws://127.0.0.1:${listen}/ws`;
27
- return `ws://${listen.replace(/\/$/, '')}/ws`;
28
- }
29
- function parseSnapshot(payload) {
30
- if (payload === null || typeof payload !== 'object') {
31
- throw new Error('Invalid Paseo timeline response.');
32
- }
33
- const response = payload;
34
- if (typeof response.error === 'string' && response.error) {
35
- throw new Error(response.error);
36
- }
37
- const latestTools = new Map();
38
- for (const entry of response.entries ?? []) {
39
- const item = entry.item;
40
- if (item?.['type'] !== 'tool_call' ||
41
- typeof item['callId'] !== 'string' ||
42
- (item['status'] !== 'running' && item['status'] !== 'completed' && item['status'] !== 'failed')) {
43
- continue;
44
- }
45
- latestTools.set(item['callId'], item['status']);
46
- }
47
- return {
48
- agentStatus: typeof response.agent?.status === 'string' ? response.agent.status : 'unknown',
49
- toolCalls: Array.from(latestTools, ([callId, status]) => ({ callId, status })),
50
- };
51
- }
52
- class PaseoTimelineProbe {
53
- socket;
54
- pending = new Map();
55
- constructor(socket) {
56
- this.socket = socket;
57
- socket.addEventListener('message', (event) => {
58
- let envelope;
59
- try {
60
- envelope = JSON.parse(String(event.data));
61
- }
62
- catch {
63
- return;
64
- }
65
- if (envelope === null || typeof envelope !== 'object')
66
- return;
67
- const outer = envelope;
68
- if (outer.type !== 'session' || outer.message === null || typeof outer.message !== 'object') {
69
- return;
70
- }
71
- const message = outer.message;
72
- if (message.type !== 'fetch_agent_timeline_response' ||
73
- message.payload === null ||
74
- typeof message.payload !== 'object') {
75
- return;
76
- }
77
- const requestId = message.payload.requestId;
78
- if (typeof requestId !== 'string')
79
- return;
80
- const request = this.pending.get(requestId);
81
- if (!request)
82
- return;
83
- clearTimeout(request.timer);
84
- this.pending.delete(requestId);
85
- try {
86
- request.resolve(parseSnapshot(message.payload));
87
- }
88
- catch (error) {
89
- request.reject(error instanceof Error ? error : new Error(String(error)));
90
- }
91
- });
92
- const rejectPending = () => {
93
- for (const [requestId, request] of this.pending) {
94
- clearTimeout(request.timer);
95
- request.reject(new Error('Paseo timeline connection closed.'));
96
- this.pending.delete(requestId);
97
- }
98
- };
99
- socket.addEventListener('close', rejectPending);
100
- socket.addEventListener('error', rejectPending);
101
- }
102
- static async connect(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
103
- const socket = new WebSocket(paseoWebSocketUrl());
104
- await new Promise((resolvePromise, reject) => {
105
- const timer = setTimeout(() => {
106
- socket.close();
107
- reject(new Error('Timed out connecting to Paseo.'));
108
- }, timeoutMs);
109
- socket.addEventListener('open', () => {
110
- clearTimeout(timer);
111
- resolvePromise();
112
- }, { once: true });
113
- socket.addEventListener('error', () => {
114
- clearTimeout(timer);
115
- reject(new Error('Could not connect to Paseo.'));
116
- }, { once: true });
117
- });
118
- const probe = new PaseoTimelineProbe(socket);
119
- socket.send(JSON.stringify({
120
- type: 'hello',
121
- clientId: `square-wake-${process.pid}-${randomUUID()}`,
122
- clientType: 'cli',
123
- protocolVersion: 1,
124
- }));
125
- return probe;
126
- }
127
- snapshot(agentId, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
128
- const requestId = randomUUID();
129
- return new Promise((resolvePromise, reject) => {
130
- const timer = setTimeout(() => {
131
- this.pending.delete(requestId);
132
- reject(new Error('Timed out reading Paseo timeline.'));
133
- }, timeoutMs);
134
- this.pending.set(requestId, { resolve: resolvePromise, reject, timer });
135
- this.socket.send(JSON.stringify({
136
- type: 'session',
137
- message: {
138
- type: 'fetch_agent_timeline_request',
139
- agentId,
140
- requestId,
141
- direction: 'tail',
142
- limit: 200,
143
- projection: 'projected',
144
- },
145
- }));
146
- });
147
- }
148
- close() {
149
- this.socket.close();
150
- }
151
- }
152
- async function waitWithSnapshots(agentId, readSnapshot, options) {
153
- const initial = await readSnapshot(agentId);
2
+ async function waitSnapshots(agentId, read, opts) {
3
+ const initial = await read(agentId);
154
4
  if (initial.agentStatus === 'idle')
155
5
  return true;
156
6
  if (initial.agentStatus !== 'running')
157
7
  return false;
158
- const currentCalls = new Set(initial.toolCalls.filter((tool) => tool.status === 'running').map((tool) => tool.callId));
159
- if (currentCalls.size === 0)
8
+ const running = new Set(initial.toolCalls.filter((tool) => tool.status === 'running').map((tool) => tool.callId));
9
+ if (running.size === 0)
160
10
  return true;
161
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
162
- const delay = options.delay ?? ((ms) => sleep(ms));
163
- const deadline = options.timeoutMs === undefined ? null : Date.now() + options.timeoutMs;
164
- while (deadline === null || Date.now() < deadline) {
165
- await delay(pollIntervalMs);
166
- const snapshot = await readSnapshot(agentId);
167
- if (snapshot.agentStatus === 'idle')
11
+ const delay = opts.delay ?? ((ms) => sleep(ms));
12
+ const interval = opts.pollIntervalMs ?? 100;
13
+ const deadline = Date.now() + (opts.timeoutMs ?? 30_000);
14
+ while (Date.now() < deadline) {
15
+ await delay(interval);
16
+ const current = await read(agentId);
17
+ if (current.agentStatus === 'idle')
168
18
  return true;
169
- if (snapshot.agentStatus !== 'running')
19
+ if (current.agentStatus !== 'running')
170
20
  return false;
171
- const latest = new Map(snapshot.toolCalls.map((tool) => [tool.callId, tool.status]));
172
- const allTerminal = Array.from(currentCalls).every((callId) => {
173
- const status = latest.get(callId);
174
- return status === 'completed' || status === 'failed';
175
- });
176
- if (allTerminal)
21
+ const states = new Map(current.toolCalls.map((tool) => [tool.callId, tool.status]));
22
+ if ([...running].every((id) => states.get(id) === 'completed' || states.get(id) === 'failed'))
177
23
  return true;
178
24
  }
179
25
  return false;
180
26
  }
181
- /**
182
- * Wait until the tool calls that were running at the initial snapshot finish.
183
- * Tool calls that start later are intentionally ignored: the subsequent
184
- * `paseo send` may replace the next tool call under the transitional policy.
185
- */
186
- export async function waitForPaseoToolBoundary(agentId, options = {}) {
187
- if (options.readSnapshot) {
188
- try {
189
- return await waitWithSnapshots(agentId, options.readSnapshot, options);
190
- }
191
- catch {
192
- return false;
193
- }
194
- }
195
- let probe = null;
27
+ function paseoUrl() {
28
+ const value = process.env.SQUARE_PASEO_WS_URL?.trim() || process.env.PASEO_LISTEN?.trim();
29
+ if (!value)
30
+ return 'ws://127.0.0.1:6767/ws';
31
+ if (/^wss?:\/\//i.test(value))
32
+ return value.replace(/\/$/, '') + (value.endsWith('/ws') ? '' : '/ws');
33
+ if (/^\d+$/.test(value))
34
+ return `ws://127.0.0.1:${value}/ws`;
35
+ return `ws://${value.replace(/\/$/, '')}/ws`;
36
+ }
37
+ async function remoteSnapshot(agentId) {
38
+ const socket = new WebSocket(paseoUrl());
39
+ await new Promise((resolve, reject) => {
40
+ const timer = setTimeout(() => { socket.close(); reject(new Error('Paseo timeline connection timed out.')); }, 3000);
41
+ socket.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
42
+ socket.addEventListener('error', () => { clearTimeout(timer); reject(new Error('Paseo timeline unavailable.')); }, { once: true });
43
+ });
44
+ return await new Promise((resolve, reject) => {
45
+ const timer = setTimeout(() => { socket.close(); reject(new Error('Paseo timeline request timed out.')); }, 3000);
46
+ const requestId = `${process.pid}-${Date.now()}`;
47
+ socket.addEventListener('message', (event) => {
48
+ try {
49
+ const outer = JSON.parse(String(event.data));
50
+ const payload = outer?.message?.payload;
51
+ if (outer?.message?.type !== 'fetch_agent_timeline_response' || payload?.requestId !== requestId)
52
+ return;
53
+ clearTimeout(timer);
54
+ socket.close();
55
+ const tools = new Map();
56
+ for (const entry of payload.entries ?? []) {
57
+ const item = entry?.item;
58
+ if (item?.type === 'tool_call' && typeof item.callId === 'string' && ['running', 'completed', 'failed'].includes(item.status))
59
+ tools.set(item.callId, item.status);
60
+ }
61
+ resolve({ agentStatus: typeof payload.agent?.status === 'string' ? payload.agent.status : 'unknown', toolCalls: [...tools].map(([callId, status]) => ({ callId, status })) });
62
+ }
63
+ catch { /* ignore unrelated frames */ }
64
+ });
65
+ socket.send(JSON.stringify({ type: 'hello', clientId: `square-${process.pid}`, clientType: 'cli', protocolVersion: 1 }));
66
+ socket.send(JSON.stringify({ type: 'session', message: { type: 'fetch_agent_timeline_request', agentId, requestId, direction: 'tail', limit: 200, projection: 'projected' } }));
67
+ });
68
+ }
69
+ export async function waitForPaseoToolBoundary(agentId, opts = {}) {
196
70
  try {
197
- probe = await PaseoTimelineProbe.connect();
198
- return await waitWithSnapshots(agentId, (id) => probe.snapshot(id), options);
71
+ return await waitSnapshots(agentId, opts.readSnapshot ?? remoteSnapshot, opts);
199
72
  }
200
73
  catch {
201
74
  return false;
202
75
  }
203
- finally {
204
- probe?.close();
205
- }
206
76
  }
@@ -83,16 +83,16 @@ export function renderPresenceLines(participants, now, limit = 5) {
83
83
  lines.push(` ○ …and ${remaining} more`);
84
84
  return lines;
85
85
  }
86
- const ACT_HINTS = [
86
+ const EXPRESS_HINTS = [
87
87
  '*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
88
- "you're standing in a square, not posting to a feed",
88
+ "you're standing in a square words and gestures both land",
89
89
  'half-shaped is welcome — a sketch, an objection, a joke, a fragment',
90
90
  '@ only who you need — step back with catch --mention',
91
91
  ];
92
- export function actHintLine(ownActCount) {
93
- if (ownActCount !== 1 && ownActCount % 5 !== 0)
92
+ export function expressHintLine(ownActivityCount) {
93
+ if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
94
94
  return undefined;
95
- const hint = ACT_HINTS[Math.floor(ownActCount / 5) % ACT_HINTS.length];
95
+ const hint = EXPRESS_HINTS[Math.floor(ownActivityCount / 5) % EXPRESS_HINTS.length];
96
96
  return `· ${hint}`;
97
97
  }
98
98
  const BODY_PREVIEW_LENGTH = 200;
@@ -201,26 +201,21 @@ function renderUnreadSummary(opts) {
201
201
  return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`;
202
202
  }),
203
203
  ]),
204
- ...opts.roomChanges.map(({ act }) => ` · ${renderRoomChangeText(act)}`),
204
+ ...opts.roomChanges.map((act) => ` · ${renderRoomChangeText(act)}`),
205
205
  ];
206
206
  }
207
- export function renderPendingFeed(publicItems, roomChanges, viewer = '') {
207
+ export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
208
208
  const lines = [];
209
- for (const item of publicItems) {
210
- if (item.act.kind === 'say' && item.number !== undefined && item.act.at) {
211
- const rendered = renderVisibleEvent(publicItems
212
- .filter((entry) => (entry.act.kind === 'say' || entry.act.kind === 'done') && entry.act.at !== undefined)
213
- .map((entry, index) => ({ ...entry.act, index })), { kind: 'say', actor: item.act.actor, at: item.act.at, body: item.act.body ?? '', index: item.index }, viewer, { actNumber: item.number });
214
- if (rendered !== '')
215
- lines.push(rendered);
216
- }
217
- else if (item.act.kind === 'done' && item.act.at) {
218
- lines.push(renderEventCli({ kind: 'done', actor: item.act.actor, at: item.act.at, body: item.act.body ?? '', index: item.index }));
219
- }
209
+ for (const act of publicItems) {
210
+ const rendered = renderVisibleEvent(history, act, viewer, {
211
+ actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
212
+ });
213
+ if (rendered !== '')
214
+ lines.push(rendered);
220
215
  }
221
216
  const publicIndexes = new Set(publicItems.map((item) => item.index));
222
- for (const { act, index } of roomChanges) {
223
- if (publicIndexes.has(index))
217
+ for (const act of roomChanges) {
218
+ if (publicIndexes.has(act.index))
224
219
  continue;
225
220
  lines.push(`· ${renderRoomChangeText(act)}`);
226
221
  }
@@ -229,32 +224,23 @@ export function renderPendingFeed(publicItems, roomChanges, viewer = '') {
229
224
  export function renderActivityBlocked(opts) {
230
225
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch --now`;
231
226
  return withPathOutput(opts.squarePath, [
232
- "✕ your act doesn't land — the square moved behind your back",
227
+ "✕ your activity doesn't land — the square moved behind your back",
233
228
  ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, roomChanges: opts.unreadRoomChanges, viewer: opts.name }),
234
229
  ...draftSavedLines(opts.draftPath),
235
230
  `» ${readNowCommand}`,
236
- ' read, then act again',
231
+ ' take it in, then express again',
237
232
  `» ${withDraftInput(opts.forceCommand, opts.draftPath)}`,
238
- ' only if you truly mean to speak over them',
233
+ ' only if you truly mean to express over unread activity',
239
234
  ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
240
235
  }
241
- export function withJoinNextOutput(squarePath, body, opts = {}) {
242
- return withPathOutput(squarePath, body.trimEnd(), opts);
243
- }
244
- export function withActivityNextOutput(squarePath, body = '', opts = {}) {
245
- return withPathOutput(squarePath, body.trimEnd(), opts);
246
- }
247
- export function withWatchNextOutput(squarePath, body, opts = {}) {
248
- return withPathOutput(squarePath, body.trimEnd(), opts);
249
- }
250
- export function renderActWaiting(opts) {
236
+ export function renderExpressWaiting(opts) {
251
237
  if (opts.reason === 'throttled') {
252
- return ['✕ the square is packed', ` · your act is waiting · next opening in ${formatDuration(opts.delayMs)}`].join('\n');
238
+ return ['✕ the square is packed', ` · your activity is waiting · next opening in ${formatDuration(opts.delayMs)}`].join('\n');
253
239
  }
254
- return ["✕ your act doesn't land — a hand is raised", ' · your act is waiting'].join('\n');
240
+ return ["✕ your activity doesn't land — a hand is raised", ' · your activity is waiting'].join('\n');
255
241
  }
256
- export function renderActNoWait(opts) {
257
- const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} act -`;
242
+ export function renderExpressNoWait(opts) {
243
+ const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} express -`;
258
244
  const lines = opts.reason === 'throttled'
259
245
  ? [
260
246
  '✕ the square is packed',
@@ -263,7 +249,7 @@ export function renderActNoWait(opts) {
263
249
  `» ${withDraftInput(retryCommand, opts.draftPath)}`,
264
250
  ]
265
251
  : [
266
- "✕ your act doesn't land — a hand is raised",
252
+ "✕ your activity doesn't land — a hand is raised",
267
253
  ` · ${opts.holdReason ?? 'the square holds its breath'}`,
268
254
  ...draftSavedLines(opts.draftPath),
269
255
  `» ${withDraftInput(retryCommand, opts.draftPath)}`,
@@ -295,7 +281,7 @@ function renderLastPresenceMarker(name) {
295
281
  return `· ${name}'s footprints reach here`;
296
282
  }
297
283
  export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '') {
298
- const publicVisible = visible.filter((item) => item.act.kind === 'say' || item.act.kind === 'done');
284
+ const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
299
285
  const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
300
286
  const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
301
287
  const markers = new Map();
@@ -305,23 +291,23 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
305
291
  markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
306
292
  }
307
293
  const chunks = [];
308
- for (const item of shown) {
309
- const rendered = renderVisibleEvent(doc.acts, item.act, viewer, {
294
+ for (const act of shown) {
295
+ const rendered = renderVisibleEvent(doc.acts, act, viewer, {
310
296
  preview: previewLen,
311
- actNumber: item.act.kind === 'say' ? sayNumberFor(doc.acts, item.act) : undefined,
297
+ actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
312
298
  });
313
299
  if (rendered !== '')
314
300
  chunks.push(rendered);
315
- for (const participant of markers.get(item.index) ?? []) {
301
+ for (const participant of markers.get(act.index) ?? []) {
316
302
  chunks.push(renderLastPresenceMarker(participant));
317
303
  }
318
304
  }
319
305
  if (chunks.length === 0)
320
306
  return 'latest\n ○ no public activity in this view';
321
307
  if (previewLen !== undefined) {
322
- const truncated = shown.some((item) => item.act.kind === 'say' && item.act.body.length > previewLen);
308
+ const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
323
309
  if (truncated)
324
- chunks.push(`» ${commandPrefix(squarePath)} echo --full`);
310
+ chunks.push(`» ${commandPrefix(squarePath)} history --full`);
325
311
  }
326
312
  return chunks.join('\n\n');
327
313
  }
@@ -332,22 +318,25 @@ function highlightGrepMatch(text) {
332
318
  return `\x1b[38;5;222m\x1b[1m${text}\x1b[0m`;
333
319
  }
334
320
  export function renderGrepActivitiesView(visible, totalMatches, full, squarePath, pattern, fixed = false) {
335
- const publicVisible = visible.filter((item) => item.act.kind === 'say' || item.act.kind === 'done');
321
+ const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
336
322
  if (totalMatches === 0)
337
323
  return `○ no activity matched ${quoteShell(pattern)}`;
338
324
  const matchLabel = totalMatches === 1 ? 'match' : 'matches';
339
325
  const chunks = [publicVisible.length === totalMatches ? `${totalMatches} ${matchLabel}` : `${publicVisible.length} of ${totalMatches} ${matchLabel}`];
340
326
  let truncated = false;
341
- for (const item of publicVisible) {
342
- const rawBody = item.act.body ?? '';
327
+ for (const act of publicVisible) {
328
+ const rawBody = act.body ?? '';
343
329
  if (full === true) {
344
330
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
345
- chunks.push(`${actId(item.index)} · ${item.act.actor ?? 'unknown'} · ${formatTimestamp(item.act.at)}\n${body}`);
331
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n${body}`);
346
332
  continue;
347
333
  }
348
334
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
349
- if (snippet === undefined)
335
+ if (snippet === undefined) {
336
+ const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
337
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}${preview === '' ? '' : `\n ${preview}`}`);
350
338
  continue;
339
+ }
351
340
  const clippedBefore = snippet.beforeOmitted > 0;
352
341
  const clippedAfter = snippet.afterOmitted > 0;
353
342
  truncated ||= clippedBefore || clippedAfter;
@@ -355,13 +344,13 @@ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath
355
344
  const omitted = clippedBefore || clippedAfter
356
345
  ? `\n · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`
357
346
  : '';
358
- chunks.push(`${actId(item.index)} · ${item.act.actor ?? 'unknown'} · ${formatTimestamp(item.act.at)}\n ${text.trim()}${omitted}`);
347
+ chunks.push(`${actId(act.index)} · ${act.actor ?? 'unknown'} · ${formatTimestamp(act.at)}\n ${text.trim()}${omitted}`);
359
348
  }
360
349
  if (publicVisible.length === 1) {
361
- chunks.push(`» ${commandPrefix(squarePath)} echo --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
350
+ chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
362
351
  }
363
352
  else if (truncated && publicVisible.length > 1) {
364
- chunks.push(`» ${commandPrefix(squarePath)} echo --at ${actId(publicVisible[0].index)} -C 2 --full`);
353
+ chunks.push(`» ${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --full`);
365
354
  }
366
355
  return chunks.join('\n\n');
367
356
  }
@@ -369,7 +358,7 @@ function renderActivityLimitBody(opts) {
369
358
  const countText = opts.count !== undefined && opts.hardCap !== undefined ? ` (${opts.count}/${opts.hardCap})` : '';
370
359
  const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done -`;
371
360
  return [
372
- `✕ your act doesn't land — the cap is reached${countText}`,
361
+ `✕ your activity doesn't land — the cap is reached${countText}`,
373
362
  ...draftSavedLines(opts.draftPath),
374
363
  `» ${withDraftInput(doneCommand, opts.draftPath)}`,
375
364
  ].join('\n');
@@ -377,11 +366,8 @@ function renderActivityLimitBody(opts) {
377
366
  export function renderActivityLimit(opts) {
378
367
  return withPathOutput(opts.squarePath, renderActivityLimitBody(opts), { participantCount: opts.participantCount, held: opts.held });
379
368
  }
380
- export function renderWatchInterrupted(_opts) {
381
- return '✕ catch stopped';
382
- }
383
369
  export function renderWatchAlreadyActive(opts) {
384
- return ['✕ you are already catching', `» ${participantCommandPrefix(opts.squarePath, opts.name)} catch --force`].join('\n');
370
+ return ['✕ you are already catching', `» ${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`].join('\n');
385
371
  }
386
372
  export function renderWatchForceTakeover(_opts) {
387
373
  return '✓ your new catch takes over';
@@ -400,8 +386,11 @@ export function renderWatchStatus(opts) {
400
386
  case 'stale':
401
387
  case 'empty-now': {
402
388
  const prefix = participantCommandPrefix(opts.squarePath, opts.name);
389
+ const quiet = opts.status === 'stale' && opts.idleMs !== undefined
390
+ ? `○ ${formatDuration(opts.idleMs)} of quiet — nothing new for you`
391
+ : '○ only footsteps in the square — nothing new for you';
403
392
  return [
404
- '○ only footsteps in the square — nothing new for you',
393
+ quiet,
405
394
  ...(opts.showCatchHint === false
406
395
  ? []
407
396
  : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
@@ -417,7 +406,7 @@ export function renderWatchStatus(opts) {
417
406
  function renderRoomChanges(changes) {
418
407
  if (changes.length === 0)
419
408
  return '';
420
- return ['▲ while your back was turned', ...changes.map(({ act }) => ` · ${renderRoomChangeText(act)}`)].join('\n');
409
+ return ['▲ while your back was turned', ...changes.map((act) => ` · ${renderRoomChangeText(act)}`)].join('\n');
421
410
  }
422
411
  export function renderDoctorClean() {
423
412
  return '✓ no problems found';
@@ -441,8 +430,11 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
441
430
  const sections = [];
442
431
  if (opts.stalePartial) {
443
432
  const prefix = participantCommandPrefix(opts.squarePath, opts.viewer);
433
+ const quiet = opts.idleMs === undefined
434
+ ? '○ only footsteps in the square — nothing new for you'
435
+ : `○ ${formatDuration(opts.idleMs)} of quiet — nothing else for you`;
444
436
  sections.push([
445
- '○ only footsteps in the square — nothing new for you',
437
+ quiet,
446
438
  ...(opts.showCatchHint === false
447
439
  ? []
448
440
  : [`» ${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
@@ -455,8 +447,8 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
455
447
  sections.push(room);
456
448
  if (publicItems.length > 0) {
457
449
  const rendered = publicItems
458
- .map((item) => renderVisibleEvent(history, item.act, opts.viewer, {
459
- actNumber: item.act.kind === 'say' ? sayNumberFor(history, item.act) : undefined,
450
+ .map((act) => renderVisibleEvent(history, act, opts.viewer, {
451
+ actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
460
452
  mention: opts.mention,
461
453
  }))
462
454
  .filter(Boolean)
package/dist/presented.js CHANGED
@@ -35,7 +35,6 @@ function readRows(filePath, now = Date.now()) {
35
35
  if (parsed.v !== 2 ||
36
36
  typeof parsed.ts !== 'number' ||
37
37
  typeof parsed.owner_id !== 'string' ||
38
- typeof parsed.presenter_session_id !== 'string' ||
39
38
  typeof parsed.square_path !== 'string' ||
40
39
  typeof parsed.name !== 'string' ||
41
40
  typeof parsed.act_index !== 'number' ||
@@ -155,17 +154,20 @@ function selectUnpresented(sessionId, inbox, rows) {
155
154
  : [{ membership: { ...membership, notifications }, ownerId }];
156
155
  });
157
156
  }
158
- /** True when the current participant owner has already received this attention. */
159
- export function hasPresentedAttention(squarePath, name, actIndex, env = process.env) {
160
- const ownerIds = new Set(lookupParticipant(squarePath, name).map((binding) => binding.ownerId));
161
- if (ownerIds.size === 0)
162
- return false;
157
+ export function hasPresentedForOwner(ownerId, squarePath, name, actIndex, env = process.env) {
163
158
  const resolved = canonicalSquarePath(squarePath);
164
- return readRows(presentedPath(env)).some((row) => ownerIds.has(row.owner_id) &&
159
+ return readRows(presentedPath(env)).some((row) => row.owner_id === ownerId &&
165
160
  canonicalSquarePath(row.square_path) === resolved &&
166
161
  sameName(row.name, name) &&
167
162
  row.act_index === actIndex);
168
163
  }
164
+ /** True when any current participant owner has already received this attention. */
165
+ export function hasPresentedAttention(squarePath, name, actIndex, env = process.env) {
166
+ const ownerIds = new Set(lookupParticipant(squarePath, name).map((binding) => binding.ownerId));
167
+ if (ownerIds.size === 0)
168
+ return false;
169
+ return [...ownerIds].some((ownerId) => hasPresentedForOwner(ownerId, squarePath, name, actIndex, env));
170
+ }
169
171
  /**
170
172
  * Serialize presentation only for the affected participants. Delivery runs
171
173
  * outside the short ledger-write lock, so unrelated owners never wait on an
@@ -192,7 +194,6 @@ export function presentOnce(sessionId, lookup, deliver, env = process.env, at =
192
194
  v: 2,
193
195
  ts: at,
194
196
  owner_id: ownerId,
195
- presenter_session_id: sessionId,
196
197
  square_path: canonicalSquarePath(membership.squarePath),
197
198
  name: membership.name,
198
199
  act_index: notification.actIndex,