clay-server 4.1.0-beta.2 → 4.1.0-beta.3

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.
@@ -0,0 +1,23 @@
1
+ function canonicalize(value) {
2
+ if (Array.isArray(value)) return value.map(canonicalize);
3
+ if (!value || typeof value !== "object") return value;
4
+ var keys = Object.keys(value).sort();
5
+ var result = {};
6
+ for (var i = 0; i < keys.length; i++) result[keys[i]] = canonicalize(value[keys[i]]);
7
+ return result;
8
+ }
9
+
10
+ function sameAskUserRequest(event, id, input) {
11
+ return !!(event && event.type === "tool_executing" && event.name === "AskUserQuestion"
12
+ && event.id === id && JSON.stringify(canonicalize(event.input || {})) === JSON.stringify(canonicalize(input || {})));
13
+ }
14
+
15
+ function hasRecordedAskUserRequest(history, id, input) {
16
+ if (!Array.isArray(history) || !id) return false;
17
+ for (var i = history.length - 1; i >= 0; i--) {
18
+ if (sameAskUserRequest(history[i], id, input)) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ module.exports = { canonicalize: canonicalize, sameAskUserRequest: sameAskUserRequest, hasRecordedAskUserRequest: hasRecordedAskUserRequest };
@@ -1,4 +1,5 @@
1
1
  var userInput = require("./yoke/user-input");
2
+ var askUserIdentity = require("./ask-user-request-identity");
2
3
 
3
4
  function attachAskUser(ctx) {
4
5
  function isFirstDebateQuestion(session) {
@@ -41,7 +42,9 @@ function attachAskUser(ctx) {
41
42
  if (boundSession.pendingAskUser && boundSession.pendingAskUser[request.id] === pending) delete boundSession.pendingAskUser[request.id];
42
43
  });
43
44
  }
44
- ctx.record(boundSession, { type: "tool_executing", id: request.id, name: "AskUserQuestion", input: input });
45
+ if (!askUserIdentity.hasRecordedAskUserRequest(boundSession.history, request.id, input)) {
46
+ ctx.record(boundSession, { type: "tool_executing", id: request.id, name: "AskUserQuestion", input: input });
47
+ }
45
48
  }
46
49
 
47
50
  function createHandler(boundSession) {
package/lib/public/app.js CHANGED
@@ -287,6 +287,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
287
287
  slashCommands: [],
288
288
  processing: false,
289
289
  activeSessionId: null,
290
+ askUserRequests: {},
290
291
  activeBackgroundTasks: [],
291
292
  sessionVendorBound: false,
292
293
  cliSessionId: null,
@@ -0,0 +1,66 @@
1
+ import { store } from './store.js';
2
+
3
+ function scopeKey() {
4
+ var sessionId = store.get('paneSessionId') || store.get('activeSessionId') || "unknown";
5
+ var project = store.get('currentSlug') || "unknown-project";
6
+ var pane = store.get('paneMode') ? "pane" : "main";
7
+ return project + "\u0000" + pane + "\u0000" + String(sessionId);
8
+ }
9
+
10
+ function requestKey(toolId) {
11
+ return scopeKey() + "\u0000" + String(toolId);
12
+ }
13
+
14
+ function requests() {
15
+ return store.get('askUserRequests') || {};
16
+ }
17
+
18
+ export function getAskUserRequestKey(toolId) {
19
+ return requestKey(toolId);
20
+ }
21
+
22
+ export function rememberAskUserRequest(toolId) {
23
+ var key = requestKey(toolId);
24
+ var current = requests();
25
+ if (!current[key]) {
26
+ var next = Object.assign({}, current);
27
+ next[key] = { toolId: String(toolId), scope: scopeKey(), answered: false };
28
+ store.set({ askUserRequests: next });
29
+ }
30
+ return key;
31
+ }
32
+
33
+ export function hasAskUserRequest(toolId) {
34
+ return !!requests()[requestKey(toolId)];
35
+ }
36
+
37
+ export function markAskUserRequestAnswered(toolId) {
38
+ var key = requestKey(toolId);
39
+ var current = requests();
40
+ if (!current[key]) return;
41
+ var next = Object.assign({}, current);
42
+ next[key] = Object.assign({}, next[key], { answered: true });
43
+ store.set({ askUserRequests: next });
44
+ }
45
+
46
+ export function findAskUserCard(root, toolId) {
47
+ if (!root || !root.querySelectorAll) return null;
48
+ var key = requestKey(toolId);
49
+ var cards = root.querySelectorAll(".ask-user-container");
50
+ for (var i = 0; i < cards.length; i++) {
51
+ if (cards[i].dataset && cards[i].dataset.askUserRequestKey === key) return cards[i];
52
+ }
53
+ return null;
54
+ }
55
+
56
+ export function saveAskUserState() {
57
+ return Object.assign({}, requests());
58
+ }
59
+
60
+ export function restoreAskUserState(saved) {
61
+ store.set({ askUserRequests: Object.assign({}, saved || {}) });
62
+ }
63
+
64
+ export function resetAskUserState() {
65
+ store.set({ askUserRequests: {} });
66
+ }
@@ -8,6 +8,7 @@ import { getChatLayout } from './theme.js';
8
8
  import { store } from './store.js';
9
9
  import { VENDOR_NAMES } from './app-rendering.js';
10
10
  import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
11
+ import { findAskUserCard, getAskUserRequestKey, hasAskUserRequest, rememberAskUserRequest, markAskUserRequestAnswered, saveAskUserState, restoreAskUserState, resetAskUserState } from './ask-user-state.js';
11
12
 
12
13
  var ctx;
13
14
 
@@ -227,6 +228,10 @@ function shortPath(p) {
227
228
 
228
229
  // --- AskUserQuestion ---
229
230
  export function renderAskUserQuestion(toolId, input) {
231
+ var existingCard = findAskUserCard(ctx.messagesEl, toolId);
232
+ if (existingCard) return existingCard;
233
+ if (hasAskUserRequest(toolId)) return null;
234
+
230
235
  ctx.finalizeAssistantBlock();
231
236
  stopThinking();
232
237
  closeToolGroup();
@@ -237,6 +242,9 @@ export function renderAskUserQuestion(toolId, input) {
237
242
  var container = document.createElement("div");
238
243
  container.className = "ask-user-container";
239
244
  container.dataset.toolId = toolId;
245
+ var requestKey = getAskUserRequestKey(toolId);
246
+ container.dataset.askUserRequestKey = requestKey;
247
+ rememberAskUserRequest(toolId);
240
248
 
241
249
  // Mate DM: wrap in avatar + content layout (same as msg-assistant)
242
250
  var mateContentWrap = null;
@@ -372,6 +380,7 @@ export function renderAskUserQuestion(toolId, input) {
372
380
  skipBtn.addEventListener("click", function () {
373
381
  if (container.classList.contains("answered")) return;
374
382
  container.classList.add("answered");
383
+ markAskUserRequestAnswered(toolId);
375
384
  enableMainInput();
376
385
  if (ctx.ws && ctx.connected) {
377
386
  ctx.ws.send(JSON.stringify({ type: "stop" }));
@@ -415,7 +424,7 @@ export function enableMainInput() {
415
424
  }
416
425
 
417
426
  function submitAskUserAnswer(container, toolId, questions, answers, multiSelections) {
418
- if (container.classList.contains("answered")) return;
427
+ if (container.classList.contains("answered") || container.dataset.askUserSubmitted === "true") return;
419
428
 
420
429
  var result = {};
421
430
  for (var i = 0; i < questions.length; i++) {
@@ -430,6 +439,7 @@ function submitAskUserAnswer(container, toolId, questions, answers, multiSelecti
430
439
  if (Object.keys(result).length === 0) return;
431
440
 
432
441
  container.classList.add("answered");
442
+ container.dataset.askUserSubmitted = "true";
433
443
  enableMainInput();
434
444
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
435
445
 
@@ -471,9 +481,10 @@ function showAnswerSummary(container, questions, answers) {
471
481
  }
472
482
 
473
483
  export function markAskUserAnswered(toolId, answers) {
474
- var container = document.querySelector('.ask-user-container[data-tool-id="' + toolId + '"]');
484
+ var container = findAskUserCard(ctx.messagesEl, toolId);
475
485
  if (container && !container.classList.contains("answered")) {
476
486
  container.classList.add("answered");
487
+ markAskUserRequestAnswered(toolId);
477
488
  enableMainInput();
478
489
  // Restore answers from history replay
479
490
  if (answers && Object.keys(answers).length > 0) {
@@ -2420,6 +2431,7 @@ export function saveToolState() {
2420
2431
  toolGroupCounter: toolGroupCounter,
2421
2432
  toolGroups: toolGroups,
2422
2433
  lastCumulativeCost: _lastCumulativeCost,
2434
+ askUserState: saveAskUserState(),
2423
2435
  };
2424
2436
  }
2425
2437
 
@@ -2435,6 +2447,7 @@ export function restoreToolState(saved) {
2435
2447
  toolGroupCounter = saved.toolGroupCounter;
2436
2448
  toolGroups = saved.toolGroups;
2437
2449
  _lastCumulativeCost = saved.lastCumulativeCost || 0;
2450
+ restoreAskUserState(saved.askUserState);
2438
2451
  if (todoWidgetEl) {
2439
2452
  setupTodoObserver();
2440
2453
  }
@@ -2442,6 +2455,7 @@ export function restoreToolState(saved) {
2442
2455
 
2443
2456
  export function resetToolState() {
2444
2457
  tools = {};
2458
+ resetAskUserState();
2445
2459
  clearThinkingState();
2446
2460
  inPlanMode = false;
2447
2461
  planContent = null;
@@ -2,6 +2,7 @@ var usersModule = require("./users");
2
2
  var yoke = require("./yoke");
3
3
  var backgroundTaskTiming = require("./background-task-timing");
4
4
  var sessionTitlePolicy = require("./session-title-policy");
5
+ var askUserIdentity = require("./ask-user-request-identity");
5
6
 
6
7
  var OVERLOAD_RETRY_LIMIT = 3;
7
8
 
@@ -379,7 +380,24 @@ function attachMessageProcessor(ctx) {
379
380
  if (block && block.type === "tool_use") {
380
381
  var input = {};
381
382
  try { input = JSON.parse(block.inputJson); } catch (e) {}
382
- sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
383
+ // Native AskUserQuestion is recorded when canUseTool opens the
384
+ // request. The later block_stop is the SDK's completion notification
385
+ // for that same request, not a second question. Keeping both records
386
+ // made replay render two independently editable cards and allowed the
387
+ // client to submit the same request twice.
388
+ var alreadyRecordedAsk = false;
389
+ if (block.name === "AskUserQuestion" && block.id && Array.isArray(session.history)) {
390
+ for (var ahi = session.history.length - 1; ahi >= 0; ahi--) {
391
+ var priorAsk = session.history[ahi];
392
+ if (askUserIdentity.sameAskUserRequest(priorAsk, block.id, input)) {
393
+ alreadyRecordedAsk = true;
394
+ break;
395
+ }
396
+ }
397
+ }
398
+ if (!alreadyRecordedAsk) {
399
+ sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
400
+ }
383
401
 
384
402
  // Track active Task tools for sub-agent done detection
385
403
  if (block.name === "Task") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0-beta.2",
3
+ "version": "4.1.0-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",