clay-server 4.0.0-beta.12 → 4.0.0-beta.13
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/project-logs-mcp-server.js +14 -12
- package/lib/project-pair-lifecycle.js +12 -8
- package/lib/project-session-pair.js +34 -34
- package/lib/project-sessions.js +2 -1
- package/lib/project-user-message.js +1 -0
- package/lib/project-worker-permission.js +3 -2
- package/lib/project.js +2 -0
- package/lib/public/app.js +0 -2
- package/lib/public/css/project-logs.css +25 -79
- package/lib/public/modules/project-logs-render.js +2 -2
- package/lib/public/modules/project-logs.js +62 -59
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/sdk-bridge.js +13 -19
- package/lib/sdk-message-processor.js +2 -3
- package/lib/server-home-chat.js +5 -0
- package/lib/session-pair-mcp-server.js +2 -0
- package/lib/session-pair-prompts.js +3 -3
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/package.json +1 -1
- package/lib/public/modules/project-logs-ambient.js +0 -238
|
@@ -21,21 +21,22 @@ var LOGS_CONTRACT =
|
|
|
21
21
|
"Do not log conversation summaries, task narration, restatements of the request, completed-work announcements, speculation, or anything the repository and its history already record. " +
|
|
22
22
|
"Every write is attributed and permanently revision-tracked, so keep entries short, concrete, and true.";
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
// rather than an option.
|
|
26
|
-
//
|
|
27
|
-
// they can reuse without asking again.
|
|
24
|
+
// User learning moments are a durable project asset, so capturing them is a
|
|
25
|
+
// default rather than an option. This category is about a change in the user's
|
|
26
|
+
// conceptual model, never knowledge the Driver acquired while doing the work.
|
|
28
27
|
var LEARNING_CONTRACT =
|
|
29
|
-
"Capture durable learning moments as Project Logs, normally under the category `learning`. " +
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
"Capture durable user learning moments as Project Logs, normally under the category `learning`. " +
|
|
29
|
+
"A learning entry is exclusively about the user's learning: the user must have engaged with a concept they did not previously know, or expressed an approximate mental model that you made more precise. It is never a record of something you, the Driver, learned or discovered while inspecting the project. " +
|
|
30
|
+
"There are two kinds. First, the user asks a conceptual question directly and the answer is durable and relevant to this project. " +
|
|
31
|
+
"Second, and easier to miss: the user describes something in their own approximate words and you identify the precise term, model, or mechanism behind it. " +
|
|
32
32
|
"If someone says the background is transparent and blurry and you name that as backdrop blur, implemented with the CSS backdrop-filter property, that is a learning moment and it should not evaporate when the conversation scrolls away. " +
|
|
33
|
-
"Record four things: the
|
|
33
|
+
"Record four things: the user's original wording or mental model, the precise concept it corresponds to, why and how it applies in this project, and any boundary or common misconception worth knowing. " +
|
|
34
34
|
"Write the title and summary so they teach at a glance: someone reading only the ledger row should come away knowing the concept. " +
|
|
35
35
|
"When it comes to learning, always capture once these criteria are met; treat it as the default rather than a judgement call. " +
|
|
36
36
|
"Never fabricate a learning moment, and never claim someone learned something they did not actually engage with. " +
|
|
37
37
|
"Attribute respectfully and factually: write that a concept was clarified in discussion. Never grade, rank, or characterise the person's knowledge. " +
|
|
38
|
-
"Do not
|
|
38
|
+
"Do not classify engineering lessons, repository discoveries, investigation outcomes, defect causes, implementation insights, decisions, or facts you learned during the work as learning; use an appropriate category such as `investigation`, `defect`, `decision`, or `reference`. " +
|
|
39
|
+
"Do not log routine command syntax, trivial confirmations, facts the user clearly already knows, or every explanation you happen to give. Capture when the user's conceptual model becomes measurably more precise. " +
|
|
39
40
|
"When new learning refines or supersedes an existing learning entry, revise that entry instead of adding a near-duplicate.";
|
|
40
41
|
|
|
41
42
|
// Sticky Notes and Project Logs are different layers, and the failure mode is
|
|
@@ -71,9 +72,10 @@ var CATEGORY_DESCRIPTION = "Record category: a short lowercase hyphen-separated
|
|
|
71
72
|
"Letters and digits in any script are accepted, so a project may keep its vocabulary in the language it works in. " +
|
|
72
73
|
"This project's own vocabulary, not a fixed list. Call list_logs or search_logs first and reuse an established category when one fits; " +
|
|
73
74
|
"coin a new concise one only when this project needs a durable distinction it does not yet have. " +
|
|
74
|
-
"Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier."
|
|
75
|
+
"Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier. " +
|
|
76
|
+
"Use `learning` only for a user learning moment described by the learning contract, never for knowledge or lessons acquired by the Driver.";
|
|
75
77
|
var PRIORITY_DESCRIPTION = "How much this outranks routine work: " + logsSchema.PRIORITIES.join(", ") + ". Defaults to normal. Priority is independent of category, so an urgent decision is both.";
|
|
76
|
-
var SUMMARY_DESCRIPTION = "One or two sentences explaining what was decided or done and why. This is what a reader sees in the ledger, so it must stand alone. For a learning entry,
|
|
78
|
+
var SUMMARY_DESCRIPTION = "One or two sentences explaining what was decided or done and why. This is what a reader sees in the ledger, so it must stand alone. For a learning entry, identify the concept the user engaged with plainly enough that the row itself teaches it.";
|
|
77
79
|
var REF_DESCRIPTION = "Opaque log reference returned by list_logs, search_logs, or create_log.";
|
|
78
80
|
|
|
79
81
|
function textResult(value) {
|
|
@@ -152,7 +154,7 @@ function projectTools(bound) {
|
|
|
152
154
|
priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
|
|
153
155
|
title: { type: "string", description: "Short factual title, plain text, written like a good commit subject." },
|
|
154
156
|
summary: { type: "string", description: SUMMARY_DESCRIPTION },
|
|
155
|
-
body: { type: "string", description: "The durable facts, context, and outcome in Markdown. No narration of your own activity. For a learning entry, cover the original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
|
|
157
|
+
body: { type: "string", description: "The durable facts, context, and outcome in Markdown. No narration of your own activity. For a learning entry, cover the user's original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
|
|
156
158
|
tags: { type: "string", description: "Optional JSON array of short tag strings." },
|
|
157
159
|
}, ["kind", "title", "summary"]),
|
|
158
160
|
handler: handler(bound, "createLog"),
|
|
@@ -130,6 +130,7 @@ function activityStatus(session) {
|
|
|
130
130
|
function attachPairLifecycle(ctx) {
|
|
131
131
|
var sm = ctx.sm;
|
|
132
132
|
var store = ctx.splitStore;
|
|
133
|
+
var turnControl = ctx.turnControl;
|
|
133
134
|
|
|
134
135
|
// Per-Driver ledger of the Worker generations it has run, newest last.
|
|
135
136
|
// Lives on the live Driver session object: it informs the Driver's next
|
|
@@ -219,7 +220,7 @@ function attachPairLifecycle(ctx) {
|
|
|
219
220
|
function partnerStatus(caller) {
|
|
220
221
|
var resolved = resolveDriverPair(caller);
|
|
221
222
|
var worker = resolved.worker;
|
|
222
|
-
var blocked = replaceBlockedReason(worker);
|
|
223
|
+
var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
|
|
223
224
|
var ledger = ledgerFor(caller).map(function (record) {
|
|
224
225
|
return {
|
|
225
226
|
generation: record.generation,
|
|
@@ -242,6 +243,7 @@ function attachPairLifecycle(ctx) {
|
|
|
242
243
|
activity: activityStatus(worker),
|
|
243
244
|
context: contextStatus(worker),
|
|
244
245
|
continuity: continuityStatus(worker),
|
|
246
|
+
orchestration: turnControl.status(caller),
|
|
245
247
|
replaceSafe: !blocked,
|
|
246
248
|
replaceBlockedReason: blocked,
|
|
247
249
|
driverTier: resolved.tier ? resolved.tier.name : null,
|
|
@@ -258,6 +260,7 @@ function attachPairLifecycle(ctx) {
|
|
|
258
260
|
var resolved = resolveDriverPair(caller);
|
|
259
261
|
var group = resolved.group;
|
|
260
262
|
var oldWorker = resolved.worker;
|
|
263
|
+
turnControl.assertWorkerAction(caller);
|
|
261
264
|
|
|
262
265
|
// Validate the replacement while the old pair is still fully intact. An
|
|
263
266
|
// uninstalled vendor, an unavailable model or an unsupported effort must
|
|
@@ -275,6 +278,9 @@ function attachPairLifecycle(ctx) {
|
|
|
275
278
|
"; call again with interrupt set to true to stop it first");
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
if (args.evaluation) validateEvaluation(args.evaluation);
|
|
282
|
+
var creationTicket = turnControl.reserveCreation(caller, "replace");
|
|
283
|
+
|
|
278
284
|
if (blocked) {
|
|
279
285
|
oldWorker.taskStopRequested = true;
|
|
280
286
|
if (oldWorker.abortController) {
|
|
@@ -290,15 +296,12 @@ function attachPairLifecycle(ctx) {
|
|
|
290
296
|
ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
|
|
291
297
|
}
|
|
292
298
|
|
|
293
|
-
// The outgoing generation is closed only after the replacement exists, so a
|
|
294
|
-
// rollback leaves its ledger entry open and its evaluation untouched. The
|
|
295
|
-
// evaluation shape is still checked here, before anything is destroyed, so
|
|
296
|
-
// a malformed assessment cannot dissolve the pair and then be rejected.
|
|
297
|
-
if (args.evaluation) validateEvaluation(args.evaluation);
|
|
298
|
-
|
|
299
299
|
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
300
300
|
var dissolved = store.dissolve(ws, { id: group.id });
|
|
301
|
-
if (!dissolved.ok)
|
|
301
|
+
if (!dissolved.ok) {
|
|
302
|
+
turnControl.releaseCreation(creationTicket);
|
|
303
|
+
throw new Error(dissolved.error || "could not dissolve the existing pair");
|
|
304
|
+
}
|
|
302
305
|
|
|
303
306
|
// History is preserved: the old Worker session stays in the project.
|
|
304
307
|
//
|
|
@@ -334,6 +337,7 @@ function attachPairLifecycle(ctx) {
|
|
|
334
337
|
var interruptNote = blocked
|
|
335
338
|
? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
|
|
336
339
|
: "";
|
|
340
|
+
turnControl.releaseCreation(creationTicket);
|
|
337
341
|
throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
|
|
338
342
|
". " + restoreNote + interruptNote);
|
|
339
343
|
}
|
|
@@ -2,12 +2,11 @@ var pairMcp = require("./session-pair-mcp-server");
|
|
|
2
2
|
var pairPrompts = require("./session-pair-prompts");
|
|
3
3
|
var { attachPairFactory } = require("./session-pair-factory");
|
|
4
4
|
var { attachPairLifecycle } = require("./project-pair-lifecycle");
|
|
5
|
+
var { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
5
6
|
var driverEligibility = require("./session-driver-eligibility");
|
|
6
7
|
var { attachWorkerPermission } = require("./project-worker-permission");
|
|
7
8
|
var { attachWorkerProposal } = require("./project-worker-proposal");
|
|
8
|
-
|
|
9
9
|
var MAX_RESPONSE_CHARS = 30000;
|
|
10
|
-
|
|
11
10
|
function toolResult(value) {
|
|
12
11
|
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
13
12
|
}
|
|
@@ -65,13 +64,10 @@ function attachSessionPair(ctx) {
|
|
|
65
64
|
var workerProposal;
|
|
66
65
|
var workerPermission;
|
|
67
66
|
var lifecycle;
|
|
68
|
-
|
|
67
|
+
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
69
68
|
function groupAndPartner(caller) {
|
|
70
69
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
71
|
-
//
|
|
72
|
-
// outlive the session they were bound to, so a stale session object — or a
|
|
73
|
-
// different object reusing the same localId — must not be able to drive a
|
|
74
|
-
// pair through a handler someone still holds a reference to.
|
|
70
|
+
// Captured handlers must remain bound to the exact live Session object.
|
|
75
71
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
76
72
|
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
77
73
|
}
|
|
@@ -106,11 +102,10 @@ function attachSessionPair(ctx) {
|
|
|
106
102
|
broadcastDelegation(group, caller, partner, false);
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
//
|
|
110
|
-
// the message is actually consumed. Shared by the delegated-result push-back
|
|
111
|
-
// and by Split Worker permission routing, so both use one resume path.
|
|
105
|
+
// Shared resume path for delegated results and Worker permission routing.
|
|
112
106
|
function resumeDriverWithMessage(caller, text, meta) {
|
|
113
107
|
if (!caller || caller.destroying) return false;
|
|
108
|
+
if (turnControl.blockedReason(caller)) return false;
|
|
114
109
|
var record = {
|
|
115
110
|
type: "user_message",
|
|
116
111
|
text: text,
|
|
@@ -165,9 +160,7 @@ function attachSessionPair(ctx) {
|
|
|
165
160
|
});
|
|
166
161
|
}
|
|
167
162
|
|
|
168
|
-
//
|
|
169
|
-
// call. Asking the wait to detach lets its turn continue; the delegated
|
|
170
|
-
// result then arrives through the existing detached push-back path.
|
|
163
|
+
// Give a waiting Driver its turn back so it can answer Worker permissions.
|
|
171
164
|
function requestDetach(worker) {
|
|
172
165
|
var token = worker && worker._pairDelegation;
|
|
173
166
|
if (!token || token.detached || token.detachRequested) return false;
|
|
@@ -216,9 +209,7 @@ function attachSessionPair(ctx) {
|
|
|
216
209
|
});
|
|
217
210
|
return;
|
|
218
211
|
}
|
|
219
|
-
//
|
|
220
|
-
// back before then (a Split Worker permission request). Both detach
|
|
221
|
-
// the same way, so the completed result still returns automatically.
|
|
212
|
+
// Timeouts and permission requests both detach into the push-back path.
|
|
222
213
|
if (Date.now() >= deadline || token.detachRequested) {
|
|
223
214
|
clearInterval(timer);
|
|
224
215
|
token.detached = true;
|
|
@@ -245,9 +236,12 @@ function attachSessionPair(ctx) {
|
|
|
245
236
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
246
237
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
247
238
|
if (!message) throw new Error("message is required");
|
|
239
|
+
turnControl.assertWorkerAction(caller);
|
|
248
240
|
var created = null;
|
|
249
241
|
if (caller && !store.groupForMember(caller.localId)) {
|
|
250
|
-
|
|
242
|
+
var creationTicket = turnControl.reserveCreation(caller, "create");
|
|
243
|
+
try { created = createWorkerForDriver(caller, args); }
|
|
244
|
+
catch (createError) { turnControl.releaseCreation(creationTicket); throw createError; }
|
|
251
245
|
}
|
|
252
246
|
var resolved = groupAndPartner(caller);
|
|
253
247
|
if (created && created.worker) lifecycle.recordGenerationStart(caller, created.worker);
|
|
@@ -367,6 +361,13 @@ function attachSessionPair(ctx) {
|
|
|
367
361
|
}
|
|
368
362
|
}
|
|
369
363
|
|
|
364
|
+
function handleHumanStop(session) {
|
|
365
|
+
var roles = turnControl.markHumanStop(session);
|
|
366
|
+
if (!roles) return false;
|
|
367
|
+
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
370
371
|
function getToolDefs(boundSession) {
|
|
371
372
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
372
373
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
@@ -377,36 +378,32 @@ function attachSessionPair(ctx) {
|
|
|
377
378
|
replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
|
|
378
379
|
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
379
380
|
});
|
|
380
|
-
// Mount
|
|
381
|
-
// lifetime of a query, and Claude queries live across turns, so gating on
|
|
382
|
-
// group membership HERE would permanently hide the tools from a session
|
|
383
|
-
// whose split is created mid-conversation (the ad-hoc flow). Handlers
|
|
384
|
-
// re-resolve the group on every call, so an ungrouped session gets a
|
|
385
|
-
// clear error, and a session that gains a partner later just works.
|
|
386
|
-
// The one structural exclusion: a session that is ALREADY a configured
|
|
387
|
-
// pair worker at query start never sees the tools.
|
|
381
|
+
// Mount for the query lifetime; handlers re-resolve live pair state.
|
|
388
382
|
var group = store.groupForMember(boundSession.localId);
|
|
389
383
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
390
|
-
//
|
|
391
|
-
// partner tools it always had. Every path that would make this session a
|
|
392
|
-
// Driver — an existing configured pair, or an unpaired session whose
|
|
393
|
-
// send_to_partner would create one — requires the hard tier invariant. A
|
|
394
|
-
// model below its family's threshold never receives the autonomous
|
|
395
|
-
// lifecycle tools and cannot create or direct a pair.
|
|
384
|
+
// Plain splits keep base tools; autonomous Drivers must pass the tier gate.
|
|
396
385
|
var adHocSplit = !!(group && !group.pair);
|
|
397
386
|
if (!adHocSplit && !driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
398
387
|
var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
|
|
399
388
|
var tools = pairMcp.getToolDefs({
|
|
400
|
-
send: function (args) {
|
|
389
|
+
send: function (args) {
|
|
390
|
+
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
391
|
+
return sendToPartner(args || {}, boundSession);
|
|
392
|
+
});
|
|
393
|
+
},
|
|
401
394
|
read: function (args) { return readPartner(args, boundSession); },
|
|
402
395
|
interrupt: function (args) { return interruptPartner(args, boundSession); },
|
|
403
396
|
close: function (args) { return closePartner(args, boundSession); },
|
|
404
397
|
status: lifecycleHandlers.status,
|
|
405
|
-
replace:
|
|
398
|
+
replace: function (args) {
|
|
399
|
+
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
400
|
+
return lifecycleHandlers.replace(args || {});
|
|
401
|
+
});
|
|
402
|
+
},
|
|
406
403
|
evaluate: lifecycleHandlers.evaluate,
|
|
407
404
|
}, { lifecycle: !adHocSplit });
|
|
408
405
|
return tools
|
|
409
|
-
.concat(workerPermission.getToolDefs(boundSession))
|
|
406
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: !group }))
|
|
410
407
|
.concat(workerProposal.getToolDefs(boundSession));
|
|
411
408
|
}
|
|
412
409
|
|
|
@@ -427,6 +424,7 @@ function attachSessionPair(ctx) {
|
|
|
427
424
|
cancelWorkerPermissions: function (worker, reason) {
|
|
428
425
|
return workerPermission.cancelForSession(worker, reason);
|
|
429
426
|
},
|
|
427
|
+
turnControl: turnControl,
|
|
430
428
|
});
|
|
431
429
|
|
|
432
430
|
workerPermission = attachWorkerPermission({
|
|
@@ -483,7 +481,9 @@ function attachSessionPair(ctx) {
|
|
|
483
481
|
}
|
|
484
482
|
|
|
485
483
|
return {
|
|
484
|
+
beginHumanTurn: turnControl.beginHumanTurn,
|
|
486
485
|
getToolDefs: getToolDefs,
|
|
486
|
+
handleHumanStop: handleHumanStop,
|
|
487
487
|
handleMessage: handleMessage,
|
|
488
488
|
handleTurnDone: handleTurnDone,
|
|
489
489
|
getSystemPrompt: getSystemPrompt,
|
package/lib/project-sessions.js
CHANGED
|
@@ -1004,7 +1004,8 @@ function attachSessions(ctx) {
|
|
|
1004
1004
|
|
|
1005
1005
|
if (msg.type === "stop") {
|
|
1006
1006
|
var session = getSessionForWs(ws);
|
|
1007
|
-
if (session && session.isProcessing) {
|
|
1007
|
+
if (session && (session.isProcessing || session._queryStarting)) {
|
|
1008
|
+
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
1008
1009
|
session.taskStopRequested = true;
|
|
1009
1010
|
if (session.abortController) session.abortController.abort();
|
|
1010
1011
|
}
|
|
@@ -331,6 +331,7 @@ function attachUserMessage(ctx) {
|
|
|
331
331
|
});
|
|
332
332
|
return true;
|
|
333
333
|
}
|
|
334
|
+
if (typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
|
|
334
335
|
if (gitAttribution) gitAttribution.beginTurn(session);
|
|
335
336
|
|
|
336
337
|
// Bind vendor to session on first message (if not already set)
|
|
@@ -328,10 +328,11 @@ function attachWorkerPermission(ctx) {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
// Exposed to the Driver only, and only while it really is a pair Driver.
|
|
331
|
-
function getToolDefs(boundSession) {
|
|
331
|
+
function getToolDefs(boundSession, options) {
|
|
332
332
|
if (!boundSession || !store) return [];
|
|
333
333
|
var group = store.groupForMember(boundSession.localId);
|
|
334
|
-
|
|
334
|
+
var dormantDriver = !!(options && options.dormantDriver);
|
|
335
|
+
if (!dormantDriver && (!group || !group.pair || group.pair.driverId !== boundSession.localId)) return [];
|
|
335
336
|
return [{
|
|
336
337
|
name: "respond_to_worker_permission",
|
|
337
338
|
description: "Approve or deny one tool-permission request raised by your paired Split Worker. " +
|
package/lib/project.js
CHANGED
|
@@ -1599,6 +1599,7 @@ function createProjectContext(opts) {
|
|
|
1599
1599
|
matesModule: matesModule,
|
|
1600
1600
|
pushModule: pushModule,
|
|
1601
1601
|
getSessionForWs: getSessionForWs,
|
|
1602
|
+
onHumanPairStop: function (session) { return _sessionPair.handleHumanStop(session); },
|
|
1602
1603
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
1603
1604
|
ensureProjectAccessForSession: ensureProjectAccessForSession,
|
|
1604
1605
|
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
@@ -1674,6 +1675,7 @@ function createProjectContext(opts) {
|
|
|
1674
1675
|
if (isMate || !_sessionPair.workerPermission) return false;
|
|
1675
1676
|
return _sessionPair.workerPermission.isDriverOperated(session);
|
|
1676
1677
|
},
|
|
1678
|
+
beginHumanPairTurn: function (session) { return _sessionPair.beginHumanTurn(session); },
|
|
1677
1679
|
cwd: cwd,
|
|
1678
1680
|
slug: slug,
|
|
1679
1681
|
isMate: isMate,
|
package/lib/public/app.js
CHANGED
|
@@ -344,8 +344,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
344
344
|
projectLogsCommentStatusEl: null,
|
|
345
345
|
projectLogsUnread: 0,
|
|
346
346
|
projectLogsSeenRevisions: {},
|
|
347
|
-
projectLogsPreview: false,
|
|
348
|
-
projectLogsPinned: false,
|
|
349
347
|
installedTools: [],
|
|
350
348
|
homeToolRegistryLoaded: false,
|
|
351
349
|
toolScanErrors: [],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Project Logs — restrained,
|
|
1
|
+
/* Project Logs — a restrained editorial ledger, designed for close reading. */
|
|
2
2
|
|
|
3
3
|
/* Wide screens: bounded workbench window beside #app, matching the geometry of
|
|
4
4
|
the document viewer and the terminal. */
|
|
@@ -94,90 +94,32 @@
|
|
|
94
94
|
.project-logs-icon-btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
|
|
95
95
|
.project-logs-icon-btn:disabled { opacity: 0.4; cursor: default; }
|
|
96
96
|
.project-logs-window-actions { display: inline-flex; align-items: center; gap: 6px; }
|
|
97
|
-
/*
|
|
98
|
-
A slim notch on the right workbench boundary, offered only where a real
|
|
99
|
-
pointer can hover. Touch keeps the explicit toolbar route. */
|
|
100
|
-
@media (min-width: 1024px) {
|
|
101
|
-
.project-logs-handle {
|
|
102
|
-
position: absolute;
|
|
103
|
-
top: 50%;
|
|
104
|
-
right: 0;
|
|
105
|
-
transform: translateY(-50%);
|
|
106
|
-
width: 14px;
|
|
107
|
-
height: 88px;
|
|
108
|
-
padding: 0;
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
justify-content: center;
|
|
112
|
-
border: 1px solid var(--border);
|
|
113
|
-
border-right: 0;
|
|
114
|
-
border-radius: 8px 0 0 8px;
|
|
115
|
-
background: var(--bg-alt);
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
z-index: 40;
|
|
118
|
-
transition: width 0.16s ease, background 0.16s ease;
|
|
119
|
-
}
|
|
120
|
-
.project-logs-handle:hover,
|
|
121
|
-
.project-logs-handle:focus-visible { width: 18px; background: var(--bg); }
|
|
122
|
-
.project-logs-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
123
|
-
.project-logs-handle.hidden { display: none; }
|
|
124
|
-
.project-logs-handle-notch {
|
|
125
|
-
width: 3px;
|
|
126
|
-
height: 34px;
|
|
127
|
-
border-radius: 999px;
|
|
128
|
-
background: var(--border);
|
|
129
|
-
transition: background 0.16s ease, height 0.16s ease;
|
|
130
|
-
}
|
|
131
|
-
.project-logs-handle:hover .project-logs-handle-notch { background: var(--text-dimmer); }
|
|
132
|
-
/* Unread: a brighter notch, no motion of its own. */
|
|
133
|
-
.project-logs-handle.has-unread .project-logs-handle-notch { background: var(--accent); height: 46px; }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Touch and narrow viewports never depend on hover. */
|
|
137
|
-
@media (max-width: 1023px) {
|
|
138
|
-
.project-logs-handle { display: none; }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* A preview is the same pane, revealed but uncommitted. */
|
|
142
|
-
#project-logs-panel.project-logs-previewing { box-shadow: 0 0 0 1px var(--accent), 4px 8px 24px rgba(var(--shadow-rgb), 0.2); }
|
|
143
|
-
|
|
144
|
-
/* --- Unread cues --- */
|
|
97
|
+
/* The explicit tool button is the only discovery affordance. */
|
|
145
98
|
#project-logs-btn.project-logs-unread { color: var(--accent); }
|
|
146
99
|
#project-logs-count { background: var(--accent); color: #fff; }
|
|
147
100
|
|
|
148
|
-
@keyframes project-logs-pulse {
|
|
149
|
-
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
|
|
150
|
-
70% { box-shadow: 0 0 0 7px transparent; }
|
|
151
|
-
100% { box-shadow: 0 0 0 0 transparent; }
|
|
152
|
-
}
|
|
153
|
-
.project-logs-pulse { animation: project-logs-pulse 1.1s ease-out 2; }
|
|
154
|
-
|
|
155
|
-
/* Reduced motion keeps the static unread marker and drops every flourish. */
|
|
156
|
-
@media (prefers-reduced-motion: reduce) {
|
|
157
|
-
.project-logs-pulse { animation: none; }
|
|
158
|
-
.project-logs-handle { transition: none; }
|
|
159
|
-
.project-logs-handle-notch { transition: none; }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
101
|
/* --- Ledger toolbar --- */
|
|
163
|
-
.project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding:
|
|
102
|
+
.project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; background: color-mix(in srgb, var(--bg-alt) 72%, transparent); }
|
|
164
103
|
.project-logs-toolbar.hidden { display: none; }
|
|
165
|
-
.project-logs-search { flex: 1; min-width: 0; height:
|
|
104
|
+
.project-logs-search { flex: 1; min-width: 0; height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-dimmer); }
|
|
166
105
|
.project-logs-search .lucide { width: 14px; flex: 0 0 auto; }
|
|
167
106
|
.project-logs-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; }
|
|
168
|
-
.project-logs-
|
|
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: 42%; 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; }
|
|
169
109
|
|
|
170
110
|
/* --- Ledger list: full pane, chronological, commit-history feel --- */
|
|
171
|
-
.project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding:
|
|
111
|
+
.project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 0 18px; }
|
|
172
112
|
.project-logs-list.hidden { display: none; }
|
|
173
|
-
.project-log-row { width: 100%; display: block; padding:
|
|
174
|
-
.project-log-row:hover { background:
|
|
113
|
+
.project-log-row { width: 100%; display: block; padding: 15px 18px 14px; border: 0; border-bottom: 1px solid var(--border-subtle); border-left: 2px solid transparent; background: transparent; color: inherit; text-align: left; cursor: pointer; transition: background 0.14s ease, border-color 0.14s ease; }
|
|
114
|
+
.project-log-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); border-left-color: var(--accent); }
|
|
115
|
+
.project-log-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
|
|
175
116
|
.project-log-row:last-child { border-bottom: 0; }
|
|
176
117
|
.project-log-row-head { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
|
|
177
|
-
.project-log-row-title { flex: 1; min-width: 0; font-size:
|
|
178
|
-
.project-log-row-summary { margin:
|
|
179
|
-
.project-log-row-meta { margin-top:
|
|
180
|
-
.project-logs-list-empty { padding:
|
|
118
|
+
.project-log-row-title { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
|
|
119
|
+
.project-log-row-summary { margin: 7px 0 0; color: var(--text-secondary); font-size: 12.5px; line-height: 1.55; }
|
|
120
|
+
.project-log-row-meta { margin-top: 8px; color: var(--text-dimmer); font-size: 10.5px; font-family: var(--font-mono); }
|
|
121
|
+
.project-logs-list-empty { max-width: 320px; margin: 54px auto; padding: 20px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); color: var(--text-dimmer); font-size: 12px; text-align: center; line-height: 1.7; }
|
|
122
|
+
.project-logs-list[aria-busy="true"] .project-logs-list-empty { color: var(--text-secondary); font-family: var(--font-mono); }
|
|
181
123
|
|
|
182
124
|
/* Category chip and priority signal. Categories are project-defined, so the
|
|
183
125
|
chip is deliberately neutral: no per-category selectors and no exhaustive
|
|
@@ -191,13 +133,13 @@
|
|
|
191
133
|
/* --- Entry detail: full pane, replaces the list --- */
|
|
192
134
|
.project-logs-detail { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; }
|
|
193
135
|
.project-logs-detail.hidden { display: none; }
|
|
194
|
-
.project-log-document { max-width:
|
|
136
|
+
.project-log-document { max-width: 760px; margin: 0 auto; padding: 32px 28px 52px; }
|
|
195
137
|
#project-logs-panel.panel-fullscreen .project-log-document { padding: clamp(28px, 5vw, 64px); }
|
|
196
138
|
.project-log-doc-chips { display: flex; align-items: center; gap: 7px; margin-bottom: 10px; }
|
|
197
|
-
.project-log-document h1 { margin: 0 0
|
|
198
|
-
.project-log-doc-summary { margin: 0 0
|
|
199
|
-
.project-log-byline { margin: 0 0
|
|
200
|
-
.project-log-markdown { line-height: 1.
|
|
139
|
+
.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
|
+
.project-log-doc-summary { margin: 0 0 13px; color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
|
|
141
|
+
.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); }
|
|
142
|
+
.project-log-markdown { font-size: 14px; line-height: 1.75; }
|
|
201
143
|
|
|
202
144
|
/* --- Discussion --- */
|
|
203
145
|
.project-log-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
|
|
@@ -230,5 +172,9 @@
|
|
|
230
172
|
|
|
231
173
|
@media (max-width: 768px) {
|
|
232
174
|
.project-logs-subtitle { display: none; }
|
|
233
|
-
.project-
|
|
175
|
+
.project-logs-topbar { gap: 8px; padding: 0 12px; }
|
|
176
|
+
.project-logs-toolbar { padding: 10px 12px; }
|
|
177
|
+
.project-logs-filter { max-width: 44%; }
|
|
178
|
+
.project-log-row { padding: 14px 14px 13px; }
|
|
179
|
+
.project-log-document { padding: 24px 18px 38px; }
|
|
234
180
|
}
|
|
@@ -159,12 +159,12 @@ function renderRow(entry, onOpen) {
|
|
|
159
159
|
return row;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export function renderList(listEl, entries, onOpen) {
|
|
162
|
+
export function renderList(listEl, entries, onOpen, emptyState) {
|
|
163
163
|
listEl.innerHTML = "";
|
|
164
164
|
if (!entries.length) {
|
|
165
165
|
var empty = document.createElement("div");
|
|
166
166
|
empty.className = "project-logs-list-empty";
|
|
167
|
-
empty.textContent = "No logs yet. This project's agent sessions record decisions and work here.";
|
|
167
|
+
empty.textContent = emptyState || "No logs yet. This project's agent sessions record decisions and work here.";
|
|
168
168
|
listEl.appendChild(empty);
|
|
169
169
|
return;
|
|
170
170
|
}
|