@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.
- package/codex-plugin/.codex-plugin/plugin.json +3 -2
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +23 -22
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +76 -0
- package/dist/cli/meta-commands.js +28 -0
- package/dist/cli/observation-commands.js +453 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +219 -0
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +6 -19
- package/dist/decisions.js +53 -86
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +68 -0
- package/dist/harness-codex.js +119 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +94 -576
- package/dist/help.js +44 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +30 -129
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +55 -63
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +26 -137
- package/dist/square-application.js +264 -0
- package/dist/square-core.js +3 -11
- package/dist/square.js +5 -1362
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +79 -138
- package/extensions/square-opencode.js +1 -1
- package/extensions/square-pi.js +8 -130
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/terminal.js +0 -125
package/dist/paseo-timeline.js
CHANGED
|
@@ -1,206 +1,76 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
-
|
|
4
|
-
const
|
|
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
|
|
159
|
-
if (
|
|
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
|
|
162
|
-
const
|
|
163
|
-
const deadline =
|
|
164
|
-
while (
|
|
165
|
-
await delay(
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
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 (
|
|
19
|
+
if (current.agentStatus !== 'running')
|
|
170
20
|
return false;
|
|
171
|
-
const
|
|
172
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -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
|
|
86
|
+
const EXPRESS_HINTS = [
|
|
87
87
|
'*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
|
|
88
|
-
"you're standing in a square
|
|
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
|
|
93
|
-
if (
|
|
92
|
+
export function expressHintLine(ownActivityCount) {
|
|
93
|
+
if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
|
|
94
94
|
return undefined;
|
|
95
|
-
const hint =
|
|
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((
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
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
|
-
'
|
|
231
|
+
' take it in, then express again',
|
|
237
232
|
`» ${withDraftInput(opts.forceCommand, opts.draftPath)}`,
|
|
238
|
-
' only if you truly mean to
|
|
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
|
|
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
|
|
238
|
+
return ['✕ the square is packed', ` · your activity is waiting · next opening in ${formatDuration(opts.delayMs)}`].join('\n');
|
|
253
239
|
}
|
|
254
|
-
return ["✕ your
|
|
240
|
+
return ["✕ your activity doesn't land — a hand is raised", ' · your activity is waiting'].join('\n');
|
|
255
241
|
}
|
|
256
|
-
export function
|
|
257
|
-
const retryCommand = `${participantCommandPrefix(opts.squarePath, opts.name)}
|
|
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
|
|
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((
|
|
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
|
|
309
|
-
const rendered = renderVisibleEvent(doc.acts,
|
|
294
|
+
for (const act of shown) {
|
|
295
|
+
const rendered = renderVisibleEvent(doc.acts, act, viewer, {
|
|
310
296
|
preview: previewLen,
|
|
311
|
-
actNumber:
|
|
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(
|
|
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((
|
|
308
|
+
const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
|
|
323
309
|
if (truncated)
|
|
324
|
-
chunks.push(`» ${commandPrefix(squarePath)}
|
|
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((
|
|
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
|
|
342
|
-
const rawBody =
|
|
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(
|
|
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(
|
|
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)}
|
|
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)}
|
|
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
|
|
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 --
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
459
|
-
actNumber:
|
|
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
|
-
|
|
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) =>
|
|
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,
|