clay-server 4.0.0-beta.16 → 4.0.0-beta.18
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/lib/daemon-projects.js +10 -5
- package/lib/daemon.js +61 -10
- package/lib/project-log-context.js +171 -0
- package/lib/project-log-feedback-delivery.js +47 -5
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +15 -4
- package/lib/project-logs-query.js +18 -0
- package/lib/project-logs-root.js +8 -0
- package/lib/project-logs-service.js +121 -15
- package/lib/project-logs-snapshot.js +3 -0
- package/lib/project-logs-store.js +29 -32
- package/lib/project-logs-versioning.js +6 -2
- package/lib/project-logs.js +61 -13
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-user-message.js +11 -0
- package/lib/project.js +6 -1
- package/lib/public/app.js +5 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/project-logs.css +20 -2
- package/lib/public/css/sticky-notes.css +7 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +22 -3
- package/lib/public/modules/app-messages.js +29 -1
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/input.js +6 -1
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/project-logs-render.js +48 -2
- package/lib/public/modules/project-logs.js +140 -11
- package/lib/public/modules/sidebar-session-hierarchy.js +16 -7
- package/lib/public/modules/sidebar-sessions.js +3 -1
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +2 -0
- package/lib/public/modules/sticky-notes-card.js +2 -0
- package/lib/public/modules/sticky-notes-editor.js +3 -0
- package/lib/public/modules/sticky-notes.js +2 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +20 -10
- package/lib/server-global-ws.js +4 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +9 -2
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server.js +14 -3
- package/lib/ws-schema.js +8 -2
- package/package.json +1 -1
package/lib/project-logs.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Authorship is split deliberately. Canonical entries are created and revised
|
|
9
9
|
// only by this project's agent sessions through `clay-logs`; a connected human
|
|
10
|
-
// reads the ledger
|
|
11
|
-
//
|
|
10
|
+
// reads the ledger, appends attributed comments, and lets the project owner
|
|
11
|
+
// delete an entry. That keeps the log an honest record rather than a wiki.
|
|
12
12
|
|
|
13
13
|
var logsMcp = require("./project-logs-mcp-server");
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ var MAX_NOTICE_SUMMARY_CHARS = 240;
|
|
|
19
19
|
// Canonical writes that move the ledger forward. A plain comment, a
|
|
20
20
|
// clarification, and a decline are participation, not revisions, so they never
|
|
21
21
|
// notify.
|
|
22
|
-
var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert"];
|
|
22
|
+
var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert", "delete"];
|
|
23
23
|
|
|
24
24
|
function clip(value, max) {
|
|
25
25
|
if (typeof value !== "string") return "";
|
|
@@ -71,6 +71,7 @@ var HANDLED_TYPES = [
|
|
|
71
71
|
"project_logs_list",
|
|
72
72
|
"project_log_read",
|
|
73
73
|
"project_log_comment",
|
|
74
|
+
"project_log_delete",
|
|
74
75
|
// Retired human mutations. Still claimed so an older client gets an
|
|
75
76
|
// explicit refusal rather than silence.
|
|
76
77
|
"project_log_create",
|
|
@@ -105,6 +106,21 @@ function attachProjectLogs(ctx) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function broadcastCommentReview(entry, request) {
|
|
110
|
+
if (isMate || !entry || !request || !request.commentId) return;
|
|
111
|
+
var notice = {
|
|
112
|
+
type: "project_log_comment_reviewed",
|
|
113
|
+
ref: entry.ref,
|
|
114
|
+
commentId: request.commentId,
|
|
115
|
+
action: request.action,
|
|
116
|
+
};
|
|
117
|
+
var clients = getClients();
|
|
118
|
+
for (var client of clients) {
|
|
119
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
120
|
+
sendTo(client, notice);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
// Wrap a canonical write so a successful one notifies. A throw never reaches
|
|
109
125
|
// here, so a failed or no-op write is silent.
|
|
110
126
|
function notifying(op, run) {
|
|
@@ -149,13 +165,27 @@ function attachProjectLogs(ctx) {
|
|
|
149
165
|
// read and neither path dumps a record body into the list.
|
|
150
166
|
// Returns both the rows and this project's live category vocabulary, so the
|
|
151
167
|
// filter is populated solely from what this project actually uses.
|
|
152
|
-
function listEntries(bound, query, category) {
|
|
168
|
+
function listEntries(bound, query, category, contextMode) {
|
|
169
|
+
var options = { kind: category, limit: LIST_LIMIT, contextMode: contextMode };
|
|
153
170
|
if (query) {
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
options.query = query;
|
|
172
|
+
var hits = bound.searchLogs(options);
|
|
173
|
+
return {
|
|
174
|
+
entries: hits.results,
|
|
175
|
+
categories: hits.categories || [],
|
|
176
|
+
contextMode: hits.contextMode,
|
|
177
|
+
currentChangeSetId: hits.currentChangeSetId,
|
|
178
|
+
isWorktree: hits.isWorktree,
|
|
179
|
+
};
|
|
156
180
|
}
|
|
157
|
-
var listed = bound.listLogs(
|
|
158
|
-
return {
|
|
181
|
+
var listed = bound.listLogs(options);
|
|
182
|
+
return {
|
|
183
|
+
entries: listed.entries,
|
|
184
|
+
categories: listed.categories || [],
|
|
185
|
+
contextMode: listed.contextMode,
|
|
186
|
+
currentChangeSetId: listed.currentChangeSetId,
|
|
187
|
+
isWorktree: listed.isWorktree,
|
|
188
|
+
};
|
|
159
189
|
}
|
|
160
190
|
|
|
161
191
|
function handleLogsMessage(ws, msg) {
|
|
@@ -187,27 +217,40 @@ function attachProjectLogs(ctx) {
|
|
|
187
217
|
var query = typeof msg.query === "string" ? msg.query.trim() : "";
|
|
188
218
|
// A view preference, not an authorization input: the store validates it.
|
|
189
219
|
var category = typeof msg.category === "string" && msg.category ? msg.category : null;
|
|
190
|
-
var
|
|
220
|
+
var contextMode = typeof msg.contextMode === "string" ? msg.contextMode : null;
|
|
221
|
+
var listing = listEntries(bound, query, category, contextMode);
|
|
191
222
|
sendTo(ws, {
|
|
192
223
|
type: "project_logs_state",
|
|
193
224
|
requestId: requestId,
|
|
194
225
|
entries: listing.entries,
|
|
195
226
|
categories: listing.categories,
|
|
227
|
+
contextMode: listing.contextMode,
|
|
228
|
+
currentChangeSetId: listing.currentChangeSetId,
|
|
229
|
+
isWorktree: listing.isWorktree === true,
|
|
230
|
+
canDelete: bound.canDelete === true,
|
|
196
231
|
});
|
|
197
232
|
return true;
|
|
198
233
|
}
|
|
199
234
|
|
|
200
235
|
if (msg.type === "project_log_read") {
|
|
201
|
-
sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }) });
|
|
236
|
+
sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }), canDelete: bound.canDelete === true });
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (msg.type === "project_log_delete") {
|
|
241
|
+
var removed = bound.removeLog({ ref: msg.ref });
|
|
242
|
+
sendTo(ws, { type: "project_log_deleted", requestId: requestId, ref: removed.ref });
|
|
243
|
+
broadcastUpdate(removed, "delete");
|
|
202
244
|
return true;
|
|
203
245
|
}
|
|
204
246
|
|
|
205
247
|
if (msg.type === "project_log_comment") {
|
|
206
248
|
var commented = bound.commentLog({ ref: msg.ref, body: msg.body });
|
|
207
|
-
|
|
208
|
-
try { onFeedback(commented); } catch (deliveryError) {
|
|
249
|
+
var reviewQueued = false;
|
|
250
|
+
try { reviewQueued = onFeedback(commented) === true; } catch (deliveryError) {
|
|
209
251
|
console.error("[project-logs] Failed to deliver comment:", deliveryError.message || deliveryError);
|
|
210
252
|
}
|
|
253
|
+
sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented, reviewQueued: reviewQueued });
|
|
211
254
|
return true;
|
|
212
255
|
}
|
|
213
256
|
} catch (e) {
|
|
@@ -232,6 +275,7 @@ function attachProjectLogs(ctx) {
|
|
|
232
275
|
reviewLogComment: function (args) {
|
|
233
276
|
var request = coerceToolArgs(args);
|
|
234
277
|
var result = bound.reviewLogComment(request);
|
|
278
|
+
broadcastCommentReview(result, request);
|
|
235
279
|
// Only an incorporation is a canonical revision; clarify and decline
|
|
236
280
|
// resolve a comment and change nothing.
|
|
237
281
|
if (request && request.action === "incorporate") broadcastUpdate(result, "incorporate");
|
|
@@ -322,7 +366,11 @@ function attachProjectLogs(ctx) {
|
|
|
322
366
|
}
|
|
323
367
|
|
|
324
368
|
function getSystemPrompt(session) {
|
|
325
|
-
if (
|
|
369
|
+
if (!service) return "";
|
|
370
|
+
if (isMate) {
|
|
371
|
+
if (!includeGlobal() || (session && !sessionBinding(session))) return "";
|
|
372
|
+
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.CLAY_READ_CONTRACT;
|
|
373
|
+
}
|
|
326
374
|
if (session && !sessionBinding(session)) return "";
|
|
327
375
|
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.LOGS_CONTRACT +
|
|
328
376
|
"\n" + logsMcp.LEARNING_CONTRACT +
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Idempotent acknowledgement support for ordinary project chat messages.
|
|
2
|
+
|
|
3
|
+
var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
4
|
+
|
|
5
|
+
function normalizeClientMessageId(value) {
|
|
6
|
+
if (typeof value !== "string" || !CLIENT_MESSAGE_ID_RE.test(value)) return null;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ownerKey(ws) {
|
|
11
|
+
return ws && ws._clayUser && ws._clayUser.id ? String(ws._clayUser.id) : "_default";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deliveryKey(ownerId, clientMessageId) {
|
|
15
|
+
return ownerId + ":" + clientMessageId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureRecordedMessages(session) {
|
|
19
|
+
if (session._recordedClientMessages) return session._recordedClientMessages;
|
|
20
|
+
var recorded = new Map();
|
|
21
|
+
var history = Array.isArray(session.history) ? session.history : [];
|
|
22
|
+
for (var i = 0; i < history.length; i++) {
|
|
23
|
+
var item = history[i];
|
|
24
|
+
var clientMessageId = normalizeClientMessageId(item && item.clientMessageId);
|
|
25
|
+
if (!clientMessageId) continue;
|
|
26
|
+
recorded.set(deliveryKey(item.from ? String(item.from) : "_default", clientMessageId), item);
|
|
27
|
+
}
|
|
28
|
+
session._recordedClientMessages = recorded;
|
|
29
|
+
return recorded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createProjectMessageDelivery(sendTo) {
|
|
33
|
+
function inspect(ws, session, msg) {
|
|
34
|
+
var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
|
|
35
|
+
if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
|
|
36
|
+
var ownerId = ownerKey(ws);
|
|
37
|
+
var recordedMessage = ensureRecordedMessages(session).get(deliveryKey(ownerId, clientMessageId)) || null;
|
|
38
|
+
return {
|
|
39
|
+
clientMessageId: clientMessageId,
|
|
40
|
+
ownerId: ownerId,
|
|
41
|
+
duplicate: !!recordedMessage,
|
|
42
|
+
recordedMessage: recordedMessage,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function markRecorded(session, receipt, message) {
|
|
47
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
48
|
+
ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function acknowledge(ws, receipt, message) {
|
|
52
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
53
|
+
sendTo(ws, {
|
|
54
|
+
type: "message_ack",
|
|
55
|
+
clientMessageId: receipt.clientMessageId,
|
|
56
|
+
message: message || receipt.recordedMessage || null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
inspect: inspect,
|
|
62
|
+
markRecorded: markRecorded,
|
|
63
|
+
acknowledge: acknowledge,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createProjectMessageDelivery: createProjectMessageDelivery,
|
|
69
|
+
normalizeClientMessageId: normalizeClientMessageId,
|
|
70
|
+
};
|
|
@@ -35,12 +35,14 @@ function attachUserMessage(ctx) {
|
|
|
35
35
|
var sdk = ctx.sdk;
|
|
36
36
|
var nm = ctx.nm;
|
|
37
37
|
var notesLifecycle = require("./notes-lifecycle");
|
|
38
|
+
var createProjectMessageDelivery = require("./project-message-delivery").createProjectMessageDelivery;
|
|
38
39
|
var tm = ctx.tm;
|
|
39
40
|
|
|
40
41
|
var send = ctx.send;
|
|
41
42
|
var sendTo = ctx.sendTo;
|
|
42
43
|
var sendToSession = ctx.sendToSession;
|
|
43
44
|
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
45
|
+
var messageDelivery = createProjectMessageDelivery(sendTo);
|
|
44
46
|
|
|
45
47
|
var clients = ctx.clients;
|
|
46
48
|
var opts = ctx.opts;
|
|
@@ -316,6 +318,12 @@ function attachUserMessage(ctx) {
|
|
|
316
318
|
var session = getSessionForWs(ws);
|
|
317
319
|
if (!session) return true;
|
|
318
320
|
|
|
321
|
+
var deliveryReceipt = messageDelivery.inspect(ws, session, msg);
|
|
322
|
+
if (deliveryReceipt.duplicate) {
|
|
323
|
+
messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(deliveryReceipt.recordedMessage));
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
319
327
|
// A configured Split Worker is Driver-operated: its work arrives through
|
|
320
328
|
// the Driver's delegation, not from a person typing into its pane. The UI
|
|
321
329
|
// hides the composer, but the refusal has to live here too — a legacy
|
|
@@ -375,9 +383,12 @@ function attachUserMessage(ctx) {
|
|
|
375
383
|
if (msg.pastes && msg.pastes.length > 0) {
|
|
376
384
|
userMsg2.pastes = msg.pastes;
|
|
377
385
|
}
|
|
386
|
+
if (deliveryReceipt.clientMessageId) userMsg2.clientMessageId = deliveryReceipt.clientMessageId;
|
|
378
387
|
if (!userMsg2._ts) userMsg2._ts = Date.now();
|
|
379
388
|
session.history.push(userMsg2);
|
|
380
389
|
sm.appendToSessionFile(session, userMsg2);
|
|
390
|
+
messageDelivery.markRecorded(session, deliveryReceipt, userMsg2);
|
|
391
|
+
messageDelivery.acknowledge(ws, deliveryReceipt, hydrateImageRefs(userMsg2));
|
|
381
392
|
sendToSessionOthers(ws, session.localId, hydrateImageRefs(userMsg2));
|
|
382
393
|
|
|
383
394
|
if (!session.title) {
|
package/lib/project.js
CHANGED
|
@@ -215,6 +215,7 @@ function createProjectContext(opts) {
|
|
|
215
215
|
var updateChannel = opts.updateChannel || "stable";
|
|
216
216
|
var osUsers = opts.osUsers || false;
|
|
217
217
|
var projectOwnerId = opts.projectOwnerId || null;
|
|
218
|
+
var projectKnowledgeId = opts.projectKnowledgeId || null;
|
|
218
219
|
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, external }
|
|
219
220
|
var isMate = opts.isMate || false;
|
|
220
221
|
var mateId = opts.mateId || (isMate ? path.basename(cwd) : null);
|
|
@@ -1318,7 +1319,7 @@ function createProjectContext(opts) {
|
|
|
1318
1319
|
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg);
|
|
1319
1320
|
return;
|
|
1320
1321
|
}
|
|
1321
|
-
if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve") {
|
|
1322
|
+
if (msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve" || msg.type === "home_clay_log_resolve") {
|
|
1322
1323
|
if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
|
|
1323
1324
|
return;
|
|
1324
1325
|
}
|
|
@@ -2188,6 +2189,7 @@ function createProjectContext(opts) {
|
|
|
2188
2189
|
isProcessing: hasProcessing,
|
|
2189
2190
|
pendingPermissions: pendingPermCount,
|
|
2190
2191
|
projectOwnerId: projectOwnerId,
|
|
2192
|
+
projectKnowledgeId: projectKnowledgeId,
|
|
2191
2193
|
};
|
|
2192
2194
|
if (isMate) {
|
|
2193
2195
|
status.isMate = true;
|
|
@@ -2197,6 +2199,9 @@ function createProjectContext(opts) {
|
|
|
2197
2199
|
status.isWorktree = true;
|
|
2198
2200
|
status.parentSlug = worktreeMeta.parentSlug;
|
|
2199
2201
|
status.branch = worktreeMeta.branch;
|
|
2202
|
+
status.changeSetId = worktreeMeta.changeSetId || null;
|
|
2203
|
+
status.baseCommit = worktreeMeta.baseCommit || null;
|
|
2204
|
+
status.headCommit = worktreeMeta.headCommit || null;
|
|
2200
2205
|
status.worktreeExternal = worktreeMeta.external === true;
|
|
2201
2206
|
}
|
|
2202
2207
|
if (usersModule.isMultiUser()) {
|
package/lib/public/app.js
CHANGED
|
@@ -289,6 +289,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
289
289
|
isHeadlessMode: false,
|
|
290
290
|
savedMainSlug: null,
|
|
291
291
|
connected: false,
|
|
292
|
+
pendingOutboundMessages: [],
|
|
292
293
|
homeChatMateId: null,
|
|
293
294
|
homeChatSessionId: null,
|
|
294
295
|
homeChatSessionModel: null,
|
|
@@ -326,11 +327,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
326
327
|
projectLogsSelectedRef: null,
|
|
327
328
|
projectLogsView: 'list',
|
|
328
329
|
projectLogsCategory: '',
|
|
330
|
+
projectLogsContextMode: '',
|
|
331
|
+
projectLogsIsWorktree: false,
|
|
329
332
|
projectLogsListScroll: 0,
|
|
330
333
|
projectLogsListRequestId: null,
|
|
331
334
|
projectLogsReadRequestId: null,
|
|
332
335
|
projectLogsCommentRequestId: null,
|
|
333
336
|
projectLogsCommentStatusEl: null,
|
|
337
|
+
projectLogsDeleteRequestId: null,
|
|
338
|
+
projectLogsCanDelete: false,
|
|
334
339
|
projectLogsUnread: 0,
|
|
335
340
|
projectLogsSeenRevisions: {},
|
|
336
341
|
installedTools: [],
|
|
@@ -486,7 +486,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
486
486
|
.search-clay-activity-item small { color: var(--text-dimmer); font-size: 10px; line-height: 1.35; }
|
|
487
487
|
@keyframes searchClaySpin { to { transform: rotate(360deg); } }
|
|
488
488
|
|
|
489
|
-
.clayos-session-link
|
|
489
|
+
.clayos-session-link,
|
|
490
|
+
.clayos-log-link {
|
|
490
491
|
display: inline-flex;
|
|
491
492
|
align-items: center;
|
|
492
493
|
gap: 6px;
|
|
@@ -503,7 +504,8 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
503
504
|
vertical-align: middle;
|
|
504
505
|
cursor: pointer;
|
|
505
506
|
}
|
|
506
|
-
.clayos-session-link::before
|
|
507
|
+
.clayos-session-link::before,
|
|
508
|
+
.clayos-log-link::before {
|
|
507
509
|
content: "\2197";
|
|
508
510
|
display: grid;
|
|
509
511
|
place-items: center;
|
|
@@ -514,13 +516,21 @@ body.mate-dm-active .search-clay-transcript .search-clay-message-user .bubble,
|
|
|
514
516
|
color: var(--accent);
|
|
515
517
|
font-size: 12px;
|
|
516
518
|
}
|
|
517
|
-
.clayos-
|
|
518
|
-
.clayos-session-link >
|
|
519
|
-
.clayos-
|
|
520
|
-
.clayos-session-link
|
|
521
|
-
.clayos-
|
|
522
|
-
.clayos-session-link
|
|
523
|
-
.clayos-
|
|
519
|
+
.clayos-log-link::before { content: "\2261"; }
|
|
520
|
+
.clayos-session-link > span,
|
|
521
|
+
.clayos-log-link > span { font-weight: 620; }
|
|
522
|
+
.clayos-session-link > small,
|
|
523
|
+
.clayos-log-link > small { color: var(--text-muted); font-size: 10px; }
|
|
524
|
+
.clayos-session-link:hover,
|
|
525
|
+
.clayos-log-link:hover { border-color: color-mix(in srgb, var(--accent) 52%, var(--border)); background: color-mix(in srgb, var(--accent) 11%, var(--bg-alt)); }
|
|
526
|
+
.clayos-session-link:focus-visible,
|
|
527
|
+
.clayos-log-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
528
|
+
.clayos-session-link.is-loading,
|
|
529
|
+
.clayos-log-link.is-loading { opacity: 0.62; cursor: progress; }
|
|
530
|
+
.clayos-session-link.is-loading::before,
|
|
531
|
+
.clayos-log-link.is-loading::before { animation: searchClaySpin 1.2s linear infinite; }
|
|
532
|
+
.clayos-session-link.is-error,
|
|
533
|
+
.clayos-log-link.is-error { border-color: color-mix(in srgb, var(--warning) 48%, var(--border)); }
|
|
524
534
|
|
|
525
535
|
.search-clay-pending {
|
|
526
536
|
align-self: flex-start;
|
|
@@ -105,7 +105,11 @@
|
|
|
105
105
|
.project-logs-search .lucide { width: 14px; flex: 0 0 auto; }
|
|
106
106
|
.project-logs-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; }
|
|
107
107
|
.project-logs-search:focus-within, .project-logs-filter:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent); border-color: var(--accent); }
|
|
108
|
-
.project-logs-filter { height: 36px; max-width:
|
|
108
|
+
.project-logs-filter { height: 36px; max-width: 180px; padding: 0 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
|
|
109
|
+
.project-logs-context-filter { max-width: 148px; }
|
|
110
|
+
.project-log-context-chip { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 7px; border-radius: 999px; background: var(--bg-alt); color: var(--text-secondary); font-size: 11px; font-weight: 600; }
|
|
111
|
+
.project-log-context-chip[data-status="archived"] { opacity: .72; }
|
|
112
|
+
.project-log-context-chip[data-status="merged"] { color: var(--success, #2f8f57); }
|
|
109
113
|
|
|
110
114
|
/* --- Ledger list: full pane, chronological, commit-history feel --- */
|
|
111
115
|
.project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 0 18px; }
|
|
@@ -139,8 +143,22 @@
|
|
|
139
143
|
.project-log-document h1 { margin: 0 0 10px; color: var(--text); font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); letter-spacing: -0.025em; line-height: 1.12; }
|
|
140
144
|
.project-log-doc-summary { margin: 0 0 13px; color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
|
|
141
145
|
.project-log-byline { margin: 0 0 25px; padding-bottom: 18px; border-bottom: 2px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
|
|
146
|
+
.project-log-delete-button { display: inline-flex; align-items: center; gap: 6px; margin: -11px 0 24px; padding: 6px 9px; border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border)); border-radius: 7px; background: transparent; color: var(--danger); font: inherit; font-size: 11px; font-weight: 700; cursor: pointer; }
|
|
147
|
+
.project-log-delete-button .lucide { width: 13px; }
|
|
142
148
|
.project-log-markdown { font-size: 14px; line-height: 1.75; }
|
|
143
149
|
|
|
150
|
+
.project-log-delete-dialog { position: fixed; inset: 0; z-index: 420; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgba(var(--shadow-rgb), 0.5); }
|
|
151
|
+
.project-log-delete-dialog.hidden { display: none; }
|
|
152
|
+
.project-log-delete-dialog-card { width: min(420px, 100%); padding: 20px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); box-shadow: 0 18px 50px rgba(var(--shadow-rgb), 0.3); }
|
|
153
|
+
.project-log-delete-dialog-card h2 { margin: 0 0 9px; color: var(--text); font-family: var(--font-display); font-size: 18px; }
|
|
154
|
+
.project-log-delete-dialog-card p { margin: 0; color: var(--text-secondary); font-size: 13px; line-height: 1.55; }
|
|
155
|
+
.project-log-delete-dialog-title { margin-top: 9px !important; color: var(--text) !important; font-weight: 700; overflow-wrap: anywhere; }
|
|
156
|
+
.project-log-delete-dialog-status { min-height: 17px; margin-top: 11px !important; color: var(--danger) !important; font-size: 11px !important; }
|
|
157
|
+
.project-log-delete-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
158
|
+
.project-log-delete-dialog-actions button { padding: 7px 12px; border: 1px solid var(--border); border-radius: 7px; background: var(--bg-alt); color: var(--text-secondary); font: inherit; font-size: 12px; font-weight: 700; cursor: pointer; }
|
|
159
|
+
.project-log-delete-dialog-actions button.danger { border-color: var(--danger); background: var(--danger); color: #fff; }
|
|
160
|
+
.project-log-delete-dialog-actions button:disabled { opacity: 0.5; cursor: default; }
|
|
161
|
+
|
|
144
162
|
/* --- Discussion --- */
|
|
145
163
|
.project-log-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
|
|
146
164
|
.project-log-discussion h2 { margin: 0 0 12px; color: var(--text); font-family: var(--font-display); font-size: 15px; }
|
|
@@ -174,7 +192,7 @@
|
|
|
174
192
|
.project-logs-subtitle { display: none; }
|
|
175
193
|
.project-logs-topbar { gap: 8px; padding: 0 12px; }
|
|
176
194
|
.project-logs-toolbar { padding: 10px 12px; }
|
|
177
|
-
.project-logs-filter { max-width:
|
|
195
|
+
.project-logs-filter { max-width: 42%; }
|
|
178
196
|
.project-log-row { padding: 14px 14px 13px; }
|
|
179
197
|
.project-log-document { padding: 24px 18px 38px; }
|
|
180
198
|
}
|
|
@@ -85,9 +85,13 @@
|
|
|
85
85
|
min-height: 80px;
|
|
86
86
|
border: 1px solid color-mix(in srgb, var(--note-border) calc(var(--note-opacity, 0.64) * 100%), transparent);
|
|
87
87
|
background: color-mix(in srgb, var(--note-surface) calc(var(--note-opacity, 0.64) * 100%), transparent);
|
|
88
|
+
-webkit-backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
|
|
89
|
+
backdrop-filter: blur(7px) saturate(0.92) contrast(1.02);
|
|
88
90
|
color: var(--note-ink);
|
|
89
91
|
transition: box-shadow 0.15s;
|
|
90
92
|
overflow: hidden;
|
|
93
|
+
contain: layout style;
|
|
94
|
+
will-change: transform, backdrop-filter;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
.sticky-note:hover {
|
|
@@ -798,8 +802,11 @@
|
|
|
798
802
|
|
|
799
803
|
@media (prefers-reduced-transparency: reduce) {
|
|
800
804
|
.sticky-note {
|
|
805
|
+
-webkit-backdrop-filter: none;
|
|
806
|
+
backdrop-filter: none;
|
|
801
807
|
border-color: var(--note-border);
|
|
802
808
|
background: var(--note-surface);
|
|
809
|
+
will-change: transform;
|
|
803
810
|
}
|
|
804
811
|
}
|
|
805
812
|
|
package/lib/public/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
32
32
|
</script>
|
|
33
33
|
<script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
|
|
34
|
-
<link rel="stylesheet" href="style.css?v=
|
|
34
|
+
<link rel="stylesheet" href="style.css?v=20260908-log-links2">
|
|
35
35
|
<style>
|
|
36
36
|
@media(max-width:768px){
|
|
37
37
|
/* User messages: vertical stack, avatar on top, right-aligned */
|
|
@@ -2337,7 +2337,7 @@
|
|
|
2337
2337
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
|
|
2338
2338
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
|
|
2339
2339
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
|
|
2340
|
-
<script type="module" src="app.js?v=
|
|
2340
|
+
<script type="module" src="app.js?v=20260908-log-links2"></script>
|
|
2341
2341
|
<div id="pwa-install-modal" class="pwa-modal hidden">
|
|
2342
2342
|
<div class="pwa-modal-backdrop"></div>
|
|
2343
2343
|
<div class="pwa-modal-card">
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
3
|
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
4
|
|
|
5
|
-
function showConfirmDialog(message, onConfirm) {
|
|
5
|
+
function showConfirmDialog(message, actionLabel, onConfirm) {
|
|
6
6
|
var modal = document.createElement("div");
|
|
7
7
|
modal.className = "admin-modal-overlay";
|
|
8
|
-
var html = '<div class="admin-modal">' +
|
|
8
|
+
var html = '<div class="admin-modal" role="dialog" aria-modal="true" aria-label="Confirm action">' +
|
|
9
9
|
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
10
10
|
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">' + escapeHtml(message) + '</p>' +
|
|
11
11
|
'</div>' +
|
|
12
12
|
'<div class="admin-modal-footer">' +
|
|
13
|
-
'<button class="admin-modal-save admin-modal-confirm-danger">
|
|
13
|
+
'<button class="admin-modal-save admin-modal-confirm-danger">' + escapeHtml(actionLabel) + '</button>' +
|
|
14
14
|
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
15
15
|
'</div></div>';
|
|
16
16
|
modal.innerHTML = html;
|
|
@@ -198,9 +198,9 @@ function renderUsersTab(body) {
|
|
|
198
198
|
var userId = this.dataset.userId;
|
|
199
199
|
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
200
200
|
var name = user ? (user.displayName || user.username) : "this user";
|
|
201
|
-
|
|
201
|
+
showConfirmDialog("Remove " + name + "? This cannot be undone.", "Remove user", function () {
|
|
202
202
|
removeUser(userId, body);
|
|
203
|
-
}
|
|
203
|
+
});
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
}
|
|
@@ -509,7 +509,7 @@ function renderInvitesTab(body) {
|
|
|
509
509
|
revokeBtns[k].addEventListener("click", function () {
|
|
510
510
|
var code = this.dataset.code;
|
|
511
511
|
var btn = this;
|
|
512
|
-
showConfirmDialog("Revoke this invite? The link will no longer work.", function () {
|
|
512
|
+
showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
|
|
513
513
|
btn.disabled = true;
|
|
514
514
|
fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
|
|
515
515
|
.then(function (r) { return r.json(); })
|
|
@@ -751,12 +751,12 @@ function renderSmtpTab(body, cfg) {
|
|
|
751
751
|
var removeBtn = body.querySelector("#smtp-remove");
|
|
752
752
|
if (removeBtn) {
|
|
753
753
|
removeBtn.addEventListener("click", function () {
|
|
754
|
-
|
|
754
|
+
showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
|
|
755
755
|
apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function () {
|
|
756
756
|
showToast("SMTP configuration removed");
|
|
757
757
|
loadSmtpTab(body);
|
|
758
758
|
});
|
|
759
|
-
}
|
|
759
|
+
});
|
|
760
760
|
});
|
|
761
761
|
}
|
|
762
762
|
}
|
|
@@ -39,6 +39,7 @@ var overlayGraceTimer = null;
|
|
|
39
39
|
// timer per live socket.
|
|
40
40
|
var HEARTBEAT_MS = 25000;
|
|
41
41
|
var heartbeatTimer = null;
|
|
42
|
+
var heartbeatDeadlineTimer = null;
|
|
42
43
|
var disconnectedAt = 0;
|
|
43
44
|
|
|
44
45
|
function clearOverlayGrace() {
|
|
@@ -62,13 +63,17 @@ export function stopHeartbeat() {
|
|
|
62
63
|
clearInterval(heartbeatTimer);
|
|
63
64
|
heartbeatTimer = null;
|
|
64
65
|
}
|
|
66
|
+
if (heartbeatDeadlineTimer) {
|
|
67
|
+
clearTimeout(heartbeatDeadlineTimer);
|
|
68
|
+
heartbeatDeadlineTimer = null;
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
// Started once per live socket. The socket it was started for is captured, so
|
|
68
73
|
// a timer that outlives its socket stops itself instead of writing to a stale
|
|
69
|
-
// connection. The pong the server sends back
|
|
70
|
-
//
|
|
71
|
-
//
|
|
74
|
+
// connection. The pong the server sends back keeps a proxy's upstream read
|
|
75
|
+
// timeout from expiring. A missing pong also identifies a half-open socket,
|
|
76
|
+
// which browsers can otherwise leave OPEN while silently losing sends.
|
|
72
77
|
export function startHeartbeat(socket) {
|
|
73
78
|
stopHeartbeat();
|
|
74
79
|
if (!socket) return;
|
|
@@ -78,11 +83,20 @@ export function startHeartbeat(socket) {
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {}
|
|
86
|
+
if (heartbeatDeadlineTimer) clearTimeout(heartbeatDeadlineTimer);
|
|
87
|
+
heartbeatDeadlineTimer = setTimeout(function () {
|
|
88
|
+
heartbeatDeadlineTimer = null;
|
|
89
|
+
if (getWs() === socket && socket.readyState === 1) connect();
|
|
90
|
+
}, 10000);
|
|
81
91
|
}, HEARTBEAT_MS);
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
export function initConnection() {
|
|
85
95
|
connectOverlay = document.getElementById("connect-overlay");
|
|
96
|
+
window.addEventListener("clay-message-delivery-timeout", function (event) {
|
|
97
|
+
var socket = event.detail && event.detail.socket;
|
|
98
|
+
if (!socket || getWs() === socket) connect();
|
|
99
|
+
});
|
|
86
100
|
|
|
87
101
|
// --- Reactive UI sync for connected/processing state ---
|
|
88
102
|
store.subscribe(function (state, prev) {
|
|
@@ -206,6 +220,7 @@ export function connect() {
|
|
|
206
220
|
// Tear down the previous socket's heartbeat before a new socket exists, so
|
|
207
221
|
// two sockets can never both be pinging.
|
|
208
222
|
stopHeartbeat();
|
|
223
|
+
if (ws && ws.readyState === 1) setStatus("disconnected");
|
|
209
224
|
if (ws) { ws.onclose = null; ws.close(); }
|
|
210
225
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
211
226
|
|
|
@@ -280,6 +295,10 @@ export function connect() {
|
|
|
280
295
|
blinkIO();
|
|
281
296
|
var msg;
|
|
282
297
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
298
|
+
if (msg.type === "pong" && heartbeatDeadlineTimer) {
|
|
299
|
+
clearTimeout(heartbeatDeadlineTimer);
|
|
300
|
+
heartbeatDeadlineTimer = null;
|
|
301
|
+
}
|
|
283
302
|
processMessage(msg);
|
|
284
303
|
};
|
|
285
304
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// All dependencies are direct imports; no context injection needed.
|
|
4
4
|
|
|
5
5
|
import { store } from './store.js';
|
|
6
|
+
import { acknowledgeMessage, replayPendingMessages } from './message-delivery.js';
|
|
6
7
|
import { getWs } from './ws-ref.js';
|
|
7
8
|
|
|
8
9
|
// --- Leaf module imports ---
|
|
@@ -41,7 +42,7 @@ import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTerm
|
|
|
41
42
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
|
|
42
43
|
import { refreshEmailSettings } from './user-settings.js';
|
|
43
44
|
import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
|
|
44
|
-
import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogUpdated, handleProjectLogsError } from './project-logs.js';
|
|
45
|
+
import { handleProjectLogsState, handleProjectLogEntry, handleProjectLogCommented, handleProjectLogCommentReviewed, handleProjectLogDeleted, handleProjectLogUpdated, handleProjectLogsError } from './project-logs.js';
|
|
45
46
|
import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
|
|
46
47
|
import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
|
|
47
48
|
import { checkAdminAccess } from './admin.js';
|
|
@@ -163,6 +164,20 @@ export function processMessage(msg) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
switch (msg.type) {
|
|
167
|
+
case "message_ack":
|
|
168
|
+
var acknowledgedEl = msg.clientMessageId
|
|
169
|
+
? messagesEl.querySelector('[data-client-message-id="' + msg.clientMessageId + '"]')
|
|
170
|
+
: null;
|
|
171
|
+
acknowledgeMessage(msg.clientMessageId);
|
|
172
|
+
if (!acknowledgedEl && msg.message) {
|
|
173
|
+
addUserMessage(msg.message.text, msg.message.images || null, msg.message.pastes || null, msg.message.from, msg.message.fromName);
|
|
174
|
+
var acknowledgedUsers = messagesEl.querySelectorAll(".msg-user");
|
|
175
|
+
if (acknowledgedUsers.length > 0) {
|
|
176
|
+
acknowledgedUsers[acknowledgedUsers.length - 1].dataset.clientMessageId = msg.clientMessageId;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
166
181
|
case "history_meta":
|
|
167
182
|
store.set({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
|
|
168
183
|
updateHistorySentinel();
|
|
@@ -766,6 +781,7 @@ export function processMessage(msg) {
|
|
|
766
781
|
? msg.runtimeTerminalId
|
|
767
782
|
: (typeof msg.terminalId === "number" ? msg.terminalId : null);
|
|
768
783
|
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
|
|
784
|
+
replayPendingMessages(msg.id);
|
|
769
785
|
// TUI sessions swap the chat UI for an embedded xterm running
|
|
770
786
|
// `claude` inside a real PTY. Mount or tear down before the rest of
|
|
771
787
|
// the chat-side bookkeeping runs so we don't waste work on hidden DOM.
|
|
@@ -887,6 +903,10 @@ export function processMessage(msg) {
|
|
|
887
903
|
} else {
|
|
888
904
|
addUserMessage(msg.text, msg.images || null, msg.pastes || null, msg.from, msg.fromName, msg.delegated ? msg : null);
|
|
889
905
|
}
|
|
906
|
+
if (msg.clientMessageId) {
|
|
907
|
+
var replayedUsers = messagesEl.querySelectorAll(".msg-user");
|
|
908
|
+
if (replayedUsers.length > 0) replayedUsers[replayedUsers.length - 1].dataset.clientMessageId = msg.clientMessageId;
|
|
909
|
+
}
|
|
890
910
|
// Split Worker pane: a delegated instruction starts (or replays into) a
|
|
891
911
|
// driven turn; surface the composer notice. Cleared on "done".
|
|
892
912
|
if (msg.delegated) showWorkerDelegationNotice(msg.delegatedByTitle || null);
|
|
@@ -1608,6 +1628,14 @@ export function processMessage(msg) {
|
|
|
1608
1628
|
handleProjectLogCommented(msg);
|
|
1609
1629
|
break;
|
|
1610
1630
|
|
|
1631
|
+
case "project_log_comment_reviewed":
|
|
1632
|
+
handleProjectLogCommentReviewed(msg);
|
|
1633
|
+
break;
|
|
1634
|
+
|
|
1635
|
+
case "project_log_deleted":
|
|
1636
|
+
handleProjectLogDeleted(msg);
|
|
1637
|
+
break;
|
|
1638
|
+
|
|
1611
1639
|
case "project_log_updated":
|
|
1612
1640
|
handleProjectLogUpdated(msg);
|
|
1613
1641
|
break;
|