@hachej/boring-ask-user 0.1.54 → 0.1.56
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/README.md +6 -1
- package/dist/front/index.js +401 -74
- package/dist/server/index.d.ts +42 -16
- package/dist/server/index.js +443 -97
- package/dist/shared/index.d.ts +314 -262
- package/dist/shared/index.js +18 -0
- package/dist/{types-CF72YmK-.d.ts → types-4g8PL2Y_.d.ts} +3 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ cancels.
|
|
|
23
23
|
| Panel | `ask-user.questions` ("Questions"), `placement: "center"`, chromeless |
|
|
24
24
|
| Surface resolver | kind `questions` (`ASK_USER_SURFACE_KIND`) → opens the panel |
|
|
25
25
|
| Agent tool | `ask_user` (blocking; resolves `answered` / `cancelled`) |
|
|
26
|
-
|
|
|
26
|
+
| WorkspaceBridge ops | `ask-user.v1.request`, `ask-user.v1.answer`, `ask-user.v1.cancel`, `ask-user.v1.pending`, `ask-user.v1.transcript` |
|
|
27
27
|
| Pi prompt | `pi.systemPrompt` nudges the agent to use `ask_user` over chat roleplay |
|
|
28
28
|
|
|
29
29
|
## How it's wired
|
|
@@ -74,6 +74,11 @@ The agent then calls `ask_user` with a `{ title, context?, schema }` payload:
|
|
|
74
74
|
The pane opens, the user submits, and the tool resolves with
|
|
75
75
|
`{ status: "answered", answer: { values: { env: "production" } } }`.
|
|
76
76
|
|
|
77
|
+
Submit/cancel/pending/transcript traffic goes through WorkspaceBridge
|
|
78
|
+
`ask-user.v1.*` operations. The old `questionsRoutes` helper for
|
|
79
|
+
`POST /api/v1/questions/commands` remains exported only for manual legacy
|
|
80
|
+
wiring; `createAskUserServerPlugin` does not register that route.
|
|
81
|
+
|
|
77
82
|
## Field types
|
|
78
83
|
|
|
79
84
|
`text`, `textarea`, `select`, `multiselect`, `checkbox`, `radio`, `number`.
|
package/dist/front/index.js
CHANGED
|
@@ -3,26 +3,20 @@
|
|
|
3
3
|
// src/front/index.tsx
|
|
4
4
|
import { Button, EmptyState, Notice, Pane, PaneBody, PaneFooter, PaneHeader, PaneTitle } from "@hachej/boring-ui-kit";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
useWorkspaceAttention,
|
|
9
|
-
workspaceEvents
|
|
6
|
+
WORKSPACE_COMPOSER_STOP_EVENT as WORKSPACE_COMPOSER_STOP_EVENT2,
|
|
7
|
+
workspaceComposerStopAppliesToSession as workspaceComposerStopAppliesToSession2
|
|
10
8
|
} from "@hachej/boring-workspace";
|
|
11
9
|
import {
|
|
12
10
|
definePlugin
|
|
13
11
|
} from "@hachej/boring-workspace/plugin";
|
|
14
12
|
import { HelpCircle, XCircle } from "lucide-react";
|
|
15
|
-
import {
|
|
13
|
+
import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useSyncExternalStore, useState as useState2 } from "react";
|
|
16
14
|
|
|
17
15
|
// src/shared/constants.ts
|
|
18
16
|
var ASK_USER_PLUGIN_ID = "ask-user";
|
|
19
17
|
var ASK_USER_PANEL_ID = "ask-user.questions";
|
|
20
18
|
var ASK_USER_PANEL_TITLE = "Questions";
|
|
21
19
|
var ASK_USER_SURFACE_KIND = "questions";
|
|
22
|
-
var ASK_USER_COMMAND_KINDS = {
|
|
23
|
-
SUBMIT: "questions.submit",
|
|
24
|
-
CANCEL: "questions.cancel"
|
|
25
|
-
};
|
|
26
20
|
var ASK_USER_UI_STATE_SLOTS = {
|
|
27
21
|
PENDING: "questions.pending"
|
|
28
22
|
};
|
|
@@ -41,6 +35,15 @@ var ASK_USER_SCHEMA_LIMITS = {
|
|
|
41
35
|
defaultTimeoutMs: 10 * 6e4
|
|
42
36
|
};
|
|
43
37
|
|
|
38
|
+
// src/shared/bridge.ts
|
|
39
|
+
var ASK_USER_BRIDGE_OPS = {
|
|
40
|
+
request: "ask-user.v1.request",
|
|
41
|
+
answer: "ask-user.v1.answer",
|
|
42
|
+
cancel: "ask-user.v1.cancel",
|
|
43
|
+
pending: "ask-user.v1.pending",
|
|
44
|
+
transcript: "ask-user.v1.transcript"
|
|
45
|
+
};
|
|
46
|
+
|
|
44
47
|
// src/shared/error-codes.ts
|
|
45
48
|
var ASK_USER_ERROR_CODES = {
|
|
46
49
|
PENDING_EXISTS: "ASK_USER_PENDING_EXISTS",
|
|
@@ -303,50 +306,219 @@ var QuestionsClientError = class extends Error {
|
|
|
303
306
|
code;
|
|
304
307
|
statusCode;
|
|
305
308
|
};
|
|
306
|
-
function
|
|
309
|
+
function readPendingQuestionHintsFromState(state) {
|
|
307
310
|
const slot = state?.[ASK_USER_UI_STATE_SLOTS.PENDING];
|
|
308
|
-
if (!slot || typeof slot !== "object") return
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
+
if (!slot || typeof slot !== "object") return [];
|
|
312
|
+
const hints = /* @__PURE__ */ new Map();
|
|
313
|
+
const rawSlot = slot;
|
|
314
|
+
const current = readHint(rawSlot.hint) ?? readHint(rawSlot.question);
|
|
315
|
+
if (current) hints.set(current.sessionId, current);
|
|
316
|
+
if (rawSlot.hintsBySession && typeof rawSlot.hintsBySession === "object" && !Array.isArray(rawSlot.hintsBySession)) {
|
|
317
|
+
for (const [sessionId, candidate] of Object.entries(rawSlot.hintsBySession)) {
|
|
318
|
+
const hint = readHint(candidate);
|
|
319
|
+
if (hint && hint.sessionId === sessionId) hints.set(sessionId, hint);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [...hints.values()];
|
|
323
|
+
}
|
|
324
|
+
function readHint(value) {
|
|
325
|
+
if (!value || typeof value !== "object") return null;
|
|
326
|
+
const raw = value;
|
|
327
|
+
if (typeof raw.questionId !== "string" || typeof raw.sessionId !== "string") return null;
|
|
328
|
+
const status = normalizeQuestionStatus(raw.status);
|
|
329
|
+
return { questionId: raw.questionId, sessionId: raw.sessionId, ...status === "abandoned" && raw.status === void 0 ? {} : { status } };
|
|
311
330
|
}
|
|
312
331
|
function createQuestionsClient(options = {}) {
|
|
313
|
-
async function
|
|
314
|
-
const response = await fetch(`${options.apiBaseUrl ?? ""}/api/v1/
|
|
332
|
+
async function callBridge(op, input, sessionId, idempotencyKey) {
|
|
333
|
+
const response = await fetch(`${options.apiBaseUrl ?? ""}/api/v1/workspace-bridge/call`, {
|
|
315
334
|
method: "POST",
|
|
316
|
-
headers: {
|
|
317
|
-
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
...options.headers ?? {},
|
|
338
|
+
...sessionId ? { "x-boring-session-id": sessionId } : {}
|
|
339
|
+
},
|
|
340
|
+
body: JSON.stringify({ op, input, ...idempotencyKey ? { idempotencyKey } : {} })
|
|
318
341
|
});
|
|
319
342
|
const payload = await response.json().catch(() => ({}));
|
|
320
|
-
if (!response.ok)
|
|
321
|
-
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const error = "error" in payload ? payload.error : void 0;
|
|
345
|
+
throw new QuestionsClientError(
|
|
346
|
+
error?.code ?? ASK_USER_ERROR_CODES.UI_UNAVAILABLE,
|
|
347
|
+
error?.message ?? "Question bridge call failed",
|
|
348
|
+
response.status
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (!payload.ok) {
|
|
352
|
+
throw new QuestionsClientError(
|
|
353
|
+
payload.error?.code ?? ASK_USER_ERROR_CODES.UI_UNAVAILABLE,
|
|
354
|
+
payload.error?.message ?? "Question bridge call failed",
|
|
355
|
+
response.status
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return payload.output;
|
|
322
359
|
}
|
|
323
360
|
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
361
|
+
async pending(sessionId) {
|
|
362
|
+
const output = await callBridge(
|
|
363
|
+
ASK_USER_BRIDGE_OPS.pending,
|
|
364
|
+
{ sessionId },
|
|
365
|
+
sessionId
|
|
366
|
+
);
|
|
367
|
+
return normalizeQuestion(output.pending);
|
|
368
|
+
},
|
|
369
|
+
async cancel(question) {
|
|
370
|
+
ensureAnswerToken(question);
|
|
371
|
+
return await callBridge(
|
|
372
|
+
ASK_USER_BRIDGE_OPS.cancel,
|
|
373
|
+
{
|
|
374
|
+
questionId: question.questionId,
|
|
375
|
+
sessionId: question.sessionId,
|
|
376
|
+
answerToken: question.answerToken
|
|
377
|
+
},
|
|
378
|
+
question.sessionId,
|
|
379
|
+
await deriveIdempotencyKey(ASK_USER_BRIDGE_OPS.cancel, {
|
|
380
|
+
questionId: question.questionId,
|
|
381
|
+
sessionId: question.sessionId,
|
|
382
|
+
answerToken: question.answerToken
|
|
383
|
+
})
|
|
384
|
+
);
|
|
327
385
|
},
|
|
328
|
-
submit(question, values) {
|
|
386
|
+
async submit(question, values) {
|
|
387
|
+
ensureAnswerToken(question);
|
|
329
388
|
if (!question.schema) throw new QuestionsClientError(ASK_USER_ERROR_CODES.QUESTION_NOT_READY, "Question is not ready");
|
|
330
389
|
const validation = validateQuestionValues(question.schema, values);
|
|
331
390
|
if (!validation.valid) throw new QuestionsClientError(ASK_USER_ERROR_CODES.ANSWER_INVALID, firstValidationMessage(validation));
|
|
332
|
-
return
|
|
391
|
+
return await callBridge(
|
|
392
|
+
ASK_USER_BRIDGE_OPS.answer,
|
|
393
|
+
{
|
|
394
|
+
questionId: question.questionId,
|
|
395
|
+
sessionId: question.sessionId,
|
|
396
|
+
answerToken: question.answerToken,
|
|
397
|
+
values
|
|
398
|
+
},
|
|
399
|
+
question.sessionId,
|
|
400
|
+
await deriveIdempotencyKey(ASK_USER_BRIDGE_OPS.answer, {
|
|
401
|
+
questionId: question.questionId,
|
|
402
|
+
sessionId: question.sessionId,
|
|
403
|
+
answerToken: question.answerToken,
|
|
404
|
+
values
|
|
405
|
+
})
|
|
406
|
+
);
|
|
333
407
|
}
|
|
334
408
|
};
|
|
335
409
|
}
|
|
410
|
+
function normalizeQuestion(value) {
|
|
411
|
+
if (!value || typeof value !== "object") return null;
|
|
412
|
+
const raw = value;
|
|
413
|
+
if (typeof raw.questionId !== "string" || typeof raw.sessionId !== "string") return null;
|
|
414
|
+
const schema = isAskUserFormSchema(raw.schema) ? raw.schema : void 0;
|
|
415
|
+
return {
|
|
416
|
+
questionId: raw.questionId,
|
|
417
|
+
sessionId: raw.sessionId,
|
|
418
|
+
ownerPrincipalId: typeof raw.ownerPrincipalId === "string" ? raw.ownerPrincipalId : "anonymous",
|
|
419
|
+
status: normalizeQuestionStatus(raw.status),
|
|
420
|
+
title: typeof raw.title === "string" ? raw.title : void 0,
|
|
421
|
+
context: typeof raw.context === "string" ? raw.context : void 0,
|
|
422
|
+
schema,
|
|
423
|
+
answerToken: typeof raw.answerToken === "string" ? raw.answerToken : "",
|
|
424
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
425
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async function deriveIdempotencyKey(op, inputValue) {
|
|
429
|
+
const canonical = `${op}:${stableStringify(inputValue)}`;
|
|
430
|
+
const subtle = globalThis.crypto?.subtle;
|
|
431
|
+
if (subtle && typeof subtle.digest === "function") {
|
|
432
|
+
const input = new TextEncoder().encode(canonical);
|
|
433
|
+
const digest = await subtle.digest("SHA-256", input);
|
|
434
|
+
return `ask-user-idem:${Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
435
|
+
}
|
|
436
|
+
return `ask-user-idem:${deterministicHashHex(canonical)}`;
|
|
437
|
+
}
|
|
438
|
+
function deterministicHashHex(value) {
|
|
439
|
+
let h1 = 3735928559 ^ value.length;
|
|
440
|
+
let h2 = 1103547991 ^ value.length;
|
|
441
|
+
let h3 = 3235826430 ^ value.length;
|
|
442
|
+
let h4 = 2654435769 ^ value.length;
|
|
443
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
444
|
+
const code = value.charCodeAt(i);
|
|
445
|
+
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
446
|
+
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
447
|
+
h3 = Math.imul(h3 ^ code, 2246822507);
|
|
448
|
+
h4 = Math.imul(h4 ^ code, 3266489909);
|
|
449
|
+
}
|
|
450
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
451
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h3 ^ h3 >>> 13, 3266489909);
|
|
452
|
+
h3 = Math.imul(h3 ^ h3 >>> 16, 2246822507) ^ Math.imul(h4 ^ h4 >>> 13, 3266489909);
|
|
453
|
+
h4 = Math.imul(h4 ^ h4 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
454
|
+
return [h1, h2, h3, h4].map((part) => (part >>> 0).toString(16).padStart(8, "0")).join("");
|
|
455
|
+
}
|
|
456
|
+
function stableStringify(value) {
|
|
457
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
458
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
459
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
460
|
+
}
|
|
461
|
+
function normalizeQuestionStatus(status) {
|
|
462
|
+
if (status === "pending" || status === "ready") return "ready";
|
|
463
|
+
if (status === "answered" || status === "cancelled" || status === "abandoned") return status;
|
|
464
|
+
return "abandoned";
|
|
465
|
+
}
|
|
466
|
+
function ensureAnswerToken(question) {
|
|
467
|
+
if (!question.answerToken) throw new QuestionsClientError(ASK_USER_ERROR_CODES.QUESTION_NOT_READY, "Question answer token is missing");
|
|
468
|
+
}
|
|
336
469
|
function firstValidationMessage(validation) {
|
|
337
470
|
return Object.values(validation.errors)[0] ?? "Invalid answer";
|
|
338
471
|
}
|
|
472
|
+
function isAskUserFormSchema(value) {
|
|
473
|
+
return !!value && typeof value === "object" && value.wireVersion === 1 && Array.isArray(value.fields);
|
|
474
|
+
}
|
|
339
475
|
|
|
340
|
-
// src/front/
|
|
341
|
-
import {
|
|
476
|
+
// src/front/runtime.ts
|
|
477
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
342
478
|
function createQuestionsStore() {
|
|
343
479
|
const listeners = /* @__PURE__ */ new Set();
|
|
344
|
-
|
|
480
|
+
const pendingBySession = /* @__PURE__ */ new Map();
|
|
481
|
+
const hintsBySession = /* @__PURE__ */ new Map();
|
|
482
|
+
const emit = () => {
|
|
483
|
+
for (const listener of [...listeners]) listener();
|
|
484
|
+
};
|
|
345
485
|
return {
|
|
346
|
-
getPending
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
486
|
+
getPending(sessionId) {
|
|
487
|
+
return sessionId ? pendingBySession.get(sessionId) ?? null : null;
|
|
488
|
+
},
|
|
489
|
+
setPending(question, sessionId) {
|
|
490
|
+
if (question) {
|
|
491
|
+
pendingBySession.set(question.sessionId, question);
|
|
492
|
+
hintsBySession.set(question.sessionId, { questionId: question.questionId, sessionId: question.sessionId, status: question.status });
|
|
493
|
+
} else if (sessionId) {
|
|
494
|
+
pendingBySession.delete(sessionId);
|
|
495
|
+
hintsBySession.delete(sessionId);
|
|
496
|
+
} else {
|
|
497
|
+
pendingBySession.clear();
|
|
498
|
+
hintsBySession.clear();
|
|
499
|
+
}
|
|
500
|
+
emit();
|
|
501
|
+
},
|
|
502
|
+
getPendingHints() {
|
|
503
|
+
return [...hintsBySession.values()];
|
|
504
|
+
},
|
|
505
|
+
getHydratedPendingKeys() {
|
|
506
|
+
return [...pendingBySession.values()].map((question) => `${question.sessionId}:${question.questionId}:${question.status}`);
|
|
507
|
+
},
|
|
508
|
+
setPendingHints(hints) {
|
|
509
|
+
hintsBySession.clear();
|
|
510
|
+
const authoritativeHints = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const hint of hints) {
|
|
512
|
+
hintsBySession.set(hint.sessionId, hint);
|
|
513
|
+
authoritativeHints.set(hint.sessionId, hint);
|
|
514
|
+
}
|
|
515
|
+
for (const [sessionId, question] of [...pendingBySession.entries()]) {
|
|
516
|
+
const hint = authoritativeHints.get(sessionId);
|
|
517
|
+
if (!hint || hint.questionId !== question.questionId || hint.status && hint.status !== question.status) {
|
|
518
|
+
pendingBySession.delete(sessionId);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
emit();
|
|
350
522
|
},
|
|
351
523
|
subscribe(listener) {
|
|
352
524
|
listeners.add(listener);
|
|
@@ -356,60 +528,143 @@ function createQuestionsStore() {
|
|
|
356
528
|
}
|
|
357
529
|
var sharedQuestionsStore = createQuestionsStore();
|
|
358
530
|
var QuestionsRuntimeContext = createContext2(null);
|
|
359
|
-
function sessionScopedBlockerId(sessionId) {
|
|
360
|
-
return sessionId === "default" || sessionId === "anonymous" ? void 0 : sessionId;
|
|
361
|
-
}
|
|
362
|
-
function pendingQuestionSnapshot(store) {
|
|
363
|
-
const pending = store.getPending();
|
|
364
|
-
return pending ? `${pending.sessionId}:${pending.questionId}:${pending.status}` : "none";
|
|
365
|
-
}
|
|
366
531
|
function useQuestionsRuntime() {
|
|
367
532
|
const ctx = useContext2(QuestionsRuntimeContext);
|
|
368
533
|
if (!ctx) throw new Error("askUserPlugin QuestionsPane must be rendered under AskUserProvider");
|
|
369
534
|
return ctx;
|
|
370
535
|
}
|
|
371
|
-
function
|
|
536
|
+
function pendingQuestionSnapshot(store) {
|
|
537
|
+
const hints = store.getPendingHints().map((hint) => `${hint.sessionId}:${hint.questionId}:${hint.status ?? "ready"}`).sort();
|
|
538
|
+
const hydrated = store.getHydratedPendingKeys().sort();
|
|
539
|
+
return `${hints.length ? hints.join("|") : "none"}#hydrated=${hydrated.join("|")}`;
|
|
540
|
+
}
|
|
541
|
+
function isSessionOpen(runtime, sessionId) {
|
|
542
|
+
if (runtime.openSessionIds) return runtime.openSessionIds.includes(sessionId);
|
|
543
|
+
return runtime.activeSessionId === sessionId;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/front/providerHooks.ts
|
|
547
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
548
|
+
import {
|
|
549
|
+
UI_COMMAND_EVENT,
|
|
550
|
+
WORKSPACE_ATTENTION_ACTION_EVENT,
|
|
551
|
+
WORKSPACE_COMPOSER_STOP_EVENT,
|
|
552
|
+
WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT,
|
|
553
|
+
events,
|
|
554
|
+
postUiCommand,
|
|
555
|
+
useWorkspaceAttention,
|
|
556
|
+
workspaceComposerStopAppliesToSession,
|
|
557
|
+
workspaceComposerStopTargetSessionId,
|
|
558
|
+
workspaceEvents
|
|
559
|
+
} from "@hachej/boring-workspace";
|
|
560
|
+
function useAskUserAttentionBlockers(runtime, pendingSnapshot) {
|
|
372
561
|
const { addBlocker, removeBlocker } = useWorkspaceAttention();
|
|
373
|
-
const runtime = useMemo2(() => ({ ...sharedQuestionsStore, apiBaseUrl, authHeaders }), [apiBaseUrl, authHeaders]);
|
|
374
|
-
const pendingSnapshot = useSyncExternalStore(runtime.subscribe, () => pendingQuestionSnapshot(runtime), () => "none");
|
|
375
562
|
useEffect2(() => {
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
563
|
+
const blockerIds = [];
|
|
564
|
+
for (const hint of runtime.getPendingHints()) {
|
|
565
|
+
if (hint.status && hint.status !== "ready") continue;
|
|
566
|
+
const blockerId = `${ASK_USER_PLUGIN_ID}:${hint.sessionId}:${hint.questionId}`;
|
|
567
|
+
blockerIds.push(blockerId);
|
|
568
|
+
const hydrated = runtime.getPending(hint.sessionId);
|
|
569
|
+
const isActiveHint = runtime.activeSessionId === hint.sessionId && isSessionOpen(runtime, hint.sessionId);
|
|
570
|
+
const actions = hydrated ? [{ id: "open", label: "Open Questions" }, { id: "cancel", label: "Cancel question" }] : isActiveHint ? [{ id: "open", label: "Open Questions" }] : void 0;
|
|
379
571
|
addBlocker({
|
|
380
572
|
id: blockerId,
|
|
381
|
-
reason: "
|
|
573
|
+
reason: "ask-user.question",
|
|
382
574
|
surfaceKind: ASK_USER_SURFACE_KIND,
|
|
383
|
-
target:
|
|
575
|
+
target: hint.questionId,
|
|
384
576
|
label: "Answer the question in Questions to continue",
|
|
385
|
-
sessionId:
|
|
386
|
-
|
|
577
|
+
sessionId: hint.sessionId,
|
|
578
|
+
sessionBadge: { kind: "question", label: "question", tone: "attention", priority: 10 },
|
|
579
|
+
actions
|
|
387
580
|
});
|
|
388
581
|
}
|
|
389
582
|
return () => {
|
|
390
|
-
|
|
583
|
+
for (const blockerId of blockerIds) removeBlocker(blockerId);
|
|
391
584
|
};
|
|
392
585
|
}, [addBlocker, removeBlocker, runtime, pendingSnapshot]);
|
|
586
|
+
}
|
|
587
|
+
function useAskUserAutoOpen(runtime, activeSessionId, pendingSnapshot) {
|
|
588
|
+
const autoOpenedQuestionsRef = useRef2(/* @__PURE__ */ new Set());
|
|
589
|
+
useEffect2(() => {
|
|
590
|
+
for (const hint2 of runtime.getPendingHints()) {
|
|
591
|
+
if (!isSessionOpen(runtime, hint2.sessionId)) autoOpenedQuestionsRef.current.delete(`${hint2.sessionId}:${hint2.questionId}`);
|
|
592
|
+
}
|
|
593
|
+
if (!activeSessionId || !isSessionOpen(runtime, activeSessionId)) return;
|
|
594
|
+
const hint = runtime.getPendingHints().find((candidate) => candidate.sessionId === activeSessionId);
|
|
595
|
+
if (!hint || hint.status && hint.status !== "ready") return;
|
|
596
|
+
const hydrated = runtime.getPending(activeSessionId);
|
|
597
|
+
if (!hydrated || hydrated.questionId !== hint.questionId || hydrated.status !== "ready") return;
|
|
598
|
+
const key = `${hint.sessionId}:${hint.questionId}`;
|
|
599
|
+
if (autoOpenedQuestionsRef.current.has(key)) return;
|
|
600
|
+
autoOpenedQuestionsRef.current.add(key);
|
|
601
|
+
postUiCommand({
|
|
602
|
+
kind: "openSurface",
|
|
603
|
+
params: {
|
|
604
|
+
kind: ASK_USER_SURFACE_KIND,
|
|
605
|
+
target: hint.questionId,
|
|
606
|
+
meta: { sessionId: hint.sessionId, openOnlyWhenSessionOpen: true }
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}, [activeSessionId, runtime, pendingSnapshot]);
|
|
610
|
+
}
|
|
611
|
+
function useAskUserAttentionActions(runtime) {
|
|
612
|
+
useEffect2(() => {
|
|
613
|
+
const onAction = (event) => {
|
|
614
|
+
const detail = event.detail;
|
|
615
|
+
if (!detail || detail.actionId !== "cancel" || detail.blocker.reason !== "ask-user.question") return;
|
|
616
|
+
const sessionId = detail.blocker.sessionId ?? detail.sessionId ?? runtime.activeSessionId;
|
|
617
|
+
const pending = runtime.getPending(sessionId);
|
|
618
|
+
if (!pending || detail.blocker.target && pending.questionId !== detail.blocker.target) return;
|
|
619
|
+
runtime.setPending(null, pending.sessionId);
|
|
620
|
+
void createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }).cancel(pending).catch(() => void 0);
|
|
621
|
+
};
|
|
622
|
+
window.addEventListener(WORKSPACE_ATTENTION_ACTION_EVENT, onAction);
|
|
623
|
+
return () => window.removeEventListener(WORKSPACE_ATTENTION_ACTION_EVENT, onAction);
|
|
624
|
+
}, [runtime]);
|
|
625
|
+
}
|
|
626
|
+
function useAskUserComposerStopCancel(runtime) {
|
|
393
627
|
useEffect2(() => {
|
|
394
628
|
const onStop = (event) => {
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
629
|
+
const detail = event.detail;
|
|
630
|
+
const sessionId = workspaceComposerStopTargetSessionId(detail, runtime.activeSessionId);
|
|
631
|
+
const pending = runtime.getPending(sessionId);
|
|
632
|
+
if (!pending || !workspaceComposerStopAppliesToSession(detail, pending.sessionId, {
|
|
633
|
+
fallbackSessionId: runtime.activeSessionId
|
|
634
|
+
})) return;
|
|
635
|
+
runtime.setPending(null, pending.sessionId);
|
|
399
636
|
void createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }).cancel(pending).catch(() => void 0);
|
|
400
637
|
};
|
|
401
|
-
window.addEventListener(
|
|
402
|
-
return () => window.removeEventListener(
|
|
638
|
+
window.addEventListener(WORKSPACE_COMPOSER_STOP_EVENT, onStop);
|
|
639
|
+
return () => window.removeEventListener(WORKSPACE_COMPOSER_STOP_EVENT, onStop);
|
|
403
640
|
}, [runtime]);
|
|
641
|
+
}
|
|
642
|
+
function useAskUserPendingRefresh(runtime, options) {
|
|
643
|
+
const { activeSessionId, apiBaseUrl, authHeaders } = options;
|
|
404
644
|
useEffect2(() => {
|
|
405
645
|
let stopped = false;
|
|
406
646
|
async function refreshPending() {
|
|
647
|
+
let hints = [];
|
|
407
648
|
try {
|
|
408
649
|
const response = await fetch(`${apiBaseUrl}/api/v1/ui/state`, { headers: authHeaders });
|
|
409
650
|
const state = await response.json().catch(() => null);
|
|
410
|
-
|
|
651
|
+
hints = readPendingQuestionHintsFromState(state);
|
|
652
|
+
if (!stopped && hasPendingStateSlot(state)) runtime.setPendingHints(hints);
|
|
411
653
|
} catch {
|
|
412
654
|
}
|
|
655
|
+
const sessionsToHydrate = /* @__PURE__ */ new Set();
|
|
656
|
+
if (activeSessionId) sessionsToHydrate.add(activeSessionId);
|
|
657
|
+
for (const hint of hints) {
|
|
658
|
+
if (isSessionOpen(runtime, hint.sessionId)) sessionsToHydrate.add(hint.sessionId);
|
|
659
|
+
}
|
|
660
|
+
if (sessionsToHydrate.size === 0 && hints[0]) sessionsToHydrate.add(hints[0].sessionId);
|
|
661
|
+
await Promise.all([...sessionsToHydrate].map(async (sessionId) => {
|
|
662
|
+
try {
|
|
663
|
+
await runtime.refreshPending(sessionId);
|
|
664
|
+
} catch {
|
|
665
|
+
if (!stopped) runtime.setPending(null, sessionId);
|
|
666
|
+
}
|
|
667
|
+
}));
|
|
413
668
|
}
|
|
414
669
|
const onVisibility = () => {
|
|
415
670
|
if (document.visibilityState === "visible") void refreshPending();
|
|
@@ -417,6 +672,10 @@ function AskUserProvider({ apiBaseUrl, authHeaders, children }) {
|
|
|
417
672
|
const onUiCommand = () => {
|
|
418
673
|
void refreshPending();
|
|
419
674
|
};
|
|
675
|
+
const onSurfaceOpenSkipped = (event) => {
|
|
676
|
+
const detail = event.detail;
|
|
677
|
+
if (detail?.kind === ASK_USER_SURFACE_KIND) void refreshPending();
|
|
678
|
+
};
|
|
420
679
|
let agentDataTimer = null;
|
|
421
680
|
const onAgentData = () => {
|
|
422
681
|
if (agentDataTimer) return;
|
|
@@ -426,44 +685,112 @@ function AskUserProvider({ apiBaseUrl, authHeaders, children }) {
|
|
|
426
685
|
}, 1200);
|
|
427
686
|
};
|
|
428
687
|
const offAgentData = events.on(workspaceEvents.agentData, onAgentData);
|
|
688
|
+
const offUiCommand = events.on(workspaceEvents.uiCommand, onUiCommand);
|
|
429
689
|
void refreshPending();
|
|
430
690
|
window.addEventListener("focus", refreshPending);
|
|
431
691
|
document.addEventListener("visibilitychange", onVisibility);
|
|
432
692
|
window.addEventListener(UI_COMMAND_EVENT, onUiCommand);
|
|
693
|
+
window.addEventListener(WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT, onSurfaceOpenSkipped);
|
|
433
694
|
return () => {
|
|
434
695
|
stopped = true;
|
|
435
696
|
if (agentDataTimer) clearTimeout(agentDataTimer);
|
|
436
697
|
offAgentData();
|
|
698
|
+
offUiCommand();
|
|
437
699
|
window.removeEventListener("focus", refreshPending);
|
|
438
700
|
document.removeEventListener("visibilitychange", onVisibility);
|
|
439
701
|
window.removeEventListener(UI_COMMAND_EVENT, onUiCommand);
|
|
702
|
+
window.removeEventListener(WORKSPACE_SURFACE_OPEN_SKIPPED_EVENT, onSurfaceOpenSkipped);
|
|
440
703
|
};
|
|
441
|
-
}, [apiBaseUrl, authHeaders, runtime]);
|
|
704
|
+
}, [activeSessionId, apiBaseUrl, authHeaders, runtime]);
|
|
705
|
+
}
|
|
706
|
+
function hasPendingStateSlot(state) {
|
|
707
|
+
return !!state && Object.prototype.hasOwnProperty.call(state, ASK_USER_UI_STATE_SLOTS.PENDING);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/front/index.tsx
|
|
711
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
712
|
+
function AskUserProvider({ apiBaseUrl, authHeaders, activeSessionId, openSessionIds, children }) {
|
|
713
|
+
const runtime = useMemo2(() => ({
|
|
714
|
+
...sharedQuestionsStore,
|
|
715
|
+
apiBaseUrl,
|
|
716
|
+
authHeaders,
|
|
717
|
+
activeSessionId,
|
|
718
|
+
openSessionIds,
|
|
719
|
+
async refreshPending(sessionId) {
|
|
720
|
+
const pending = await createQuestionsClient({ apiBaseUrl, headers: authHeaders }).pending(sessionId);
|
|
721
|
+
sharedQuestionsStore.setPending(pending, sessionId);
|
|
722
|
+
return pending;
|
|
723
|
+
}
|
|
724
|
+
}), [activeSessionId, apiBaseUrl, authHeaders, openSessionIds]);
|
|
725
|
+
const pendingSnapshot = useSyncExternalStore(runtime.subscribe, () => pendingQuestionSnapshot(runtime), () => "none");
|
|
726
|
+
useAskUserAttentionBlockers(runtime, pendingSnapshot);
|
|
727
|
+
useAskUserAutoOpen(runtime, activeSessionId, pendingSnapshot);
|
|
728
|
+
useAskUserAttentionActions(runtime);
|
|
729
|
+
useAskUserComposerStopCancel(runtime);
|
|
730
|
+
useAskUserPendingRefresh(runtime, { activeSessionId, apiBaseUrl, authHeaders });
|
|
442
731
|
return /* @__PURE__ */ jsx2(QuestionsRuntimeContext.Provider, { value: runtime, children });
|
|
443
732
|
}
|
|
733
|
+
function paneQuestionSessionId(runtime, params) {
|
|
734
|
+
const activeSessionId = runtime.activeSessionId ?? null;
|
|
735
|
+
if (activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId)) return activeSessionId;
|
|
736
|
+
if (params?.sessionId && isPaneSessionVisible(runtime, params.sessionId)) return params.sessionId;
|
|
737
|
+
return activeSessionId && isPaneSessionVisible(runtime, activeSessionId) ? activeSessionId : null;
|
|
738
|
+
}
|
|
739
|
+
function isPaneSessionVisible(runtime, sessionId) {
|
|
740
|
+
return !runtime.openSessionIds || isSessionOpen(runtime, sessionId);
|
|
741
|
+
}
|
|
742
|
+
function isPaneSessionKnownHidden(runtime, sessionId) {
|
|
743
|
+
return !!runtime.openSessionIds && !isSessionOpen(runtime, sessionId);
|
|
744
|
+
}
|
|
745
|
+
function hasReadyQuestion(runtime, sessionId) {
|
|
746
|
+
const pending = runtime.getPending(sessionId);
|
|
747
|
+
if (pending?.status === "ready") return true;
|
|
748
|
+
return runtime.getPendingHints().some((hint) => hint.sessionId === sessionId && (!hint.status || hint.status === "ready"));
|
|
749
|
+
}
|
|
444
750
|
function QuestionsPane({ api, params, className }) {
|
|
445
751
|
const runtime = useQuestionsRuntime();
|
|
446
|
-
const
|
|
752
|
+
const paneSessionId = paneQuestionSessionId(runtime, params);
|
|
753
|
+
const pending = useSyncExternalStore(runtime.subscribe, () => runtime.getPending(paneSessionId), () => runtime.getPending(paneSessionId));
|
|
447
754
|
const [closedQuestionId, setClosedQuestionId] = useState2(null);
|
|
755
|
+
const retargetRefreshRef = useRef3(null);
|
|
448
756
|
const [error, setError] = useState2(null);
|
|
449
757
|
const [submitting, setSubmitting] = useState2(false);
|
|
450
|
-
const
|
|
451
|
-
const question = pending ?? (paramQuestion?.questionId === closedQuestionId ? null : paramQuestion) ?? null;
|
|
758
|
+
const question = pending?.questionId === closedQuestionId ? null : pending;
|
|
452
759
|
const client = useMemo2(() => createQuestionsClient({ apiBaseUrl: runtime.apiBaseUrl, headers: runtime.authHeaders }), [runtime.apiBaseUrl, runtime.authHeaders]);
|
|
453
|
-
|
|
760
|
+
useEffect3(() => {
|
|
761
|
+
if (!params?.sessionId || !isPaneSessionKnownHidden(runtime, params.sessionId)) return;
|
|
762
|
+
const activeSessionId = runtime.activeSessionId ?? null;
|
|
763
|
+
const canShowActiveQuestion = activeSessionId && isPaneSessionVisible(runtime, activeSessionId) && hasReadyQuestion(runtime, activeSessionId);
|
|
764
|
+
if (!canShowActiveQuestion) api.close();
|
|
765
|
+
}, [api, params?.sessionId, runtime]);
|
|
766
|
+
useEffect3(() => {
|
|
454
767
|
const onStop = (event) => {
|
|
455
|
-
const
|
|
456
|
-
if (!question ||
|
|
768
|
+
const detail = event.detail;
|
|
769
|
+
if (!question || !workspaceComposerStopAppliesToSession2(detail, question.sessionId)) return;
|
|
457
770
|
setClosedQuestionId(question.questionId);
|
|
458
|
-
runtime.setPending(null);
|
|
771
|
+
runtime.setPending(null, question.sessionId);
|
|
459
772
|
api.close();
|
|
460
773
|
};
|
|
461
|
-
window.addEventListener(
|
|
462
|
-
return () => window.removeEventListener(
|
|
774
|
+
window.addEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
|
|
775
|
+
return () => window.removeEventListener(WORKSPACE_COMPOSER_STOP_EVENT2, onStop);
|
|
463
776
|
}, [api, question, runtime]);
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
777
|
+
useEffect3(() => {
|
|
778
|
+
if (!paneSessionId) return;
|
|
779
|
+
const targetQuestionId = params?.questionId;
|
|
780
|
+
if (!pending) {
|
|
781
|
+
retargetRefreshRef.current = null;
|
|
782
|
+
void runtime.refreshPending(paneSessionId).catch(() => void 0);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (!targetQuestionId || pending.questionId === targetQuestionId) {
|
|
786
|
+
retargetRefreshRef.current = null;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const refreshKey = `${paneSessionId}:${targetQuestionId}`;
|
|
790
|
+
if (retargetRefreshRef.current === refreshKey) return;
|
|
791
|
+
retargetRefreshRef.current = refreshKey;
|
|
792
|
+
void runtime.refreshPending(paneSessionId).catch(() => void 0);
|
|
793
|
+
}, [paneSessionId, params?.questionId, pending, runtime]);
|
|
467
794
|
return /* @__PURE__ */ jsx2("div", { className: className ? `${className} min-h-0 overflow-hidden` : "h-full min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxs2(Pane, { className: "h-full min-h-0 overflow-hidden border-0 bg-background text-sm", children: [
|
|
468
795
|
/* @__PURE__ */ jsx2(PaneHeader, { className: "border-b bg-background/95", children: /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsxs2(PaneTitle, { className: "flex items-center gap-2", children: [
|
|
469
796
|
/* @__PURE__ */ jsx2(HelpCircle, { className: "h-4 w-4 text-muted-foreground" }),
|
|
@@ -476,7 +803,7 @@ function QuestionsPane({ api, params, className }) {
|
|
|
476
803
|
try {
|
|
477
804
|
await client.submit(question, values);
|
|
478
805
|
setClosedQuestionId(question.questionId);
|
|
479
|
-
runtime.setPending(null);
|
|
806
|
+
runtime.setPending(null, question.sessionId);
|
|
480
807
|
api.close();
|
|
481
808
|
params?.__closeWorkbenchOnDone?.();
|
|
482
809
|
} catch (err) {
|
|
@@ -490,7 +817,7 @@ function QuestionsPane({ api, params, className }) {
|
|
|
490
817
|
try {
|
|
491
818
|
await client.cancel(question);
|
|
492
819
|
setClosedQuestionId(question.questionId);
|
|
493
|
-
runtime.setPending(null);
|
|
820
|
+
runtime.setPending(null, question.sessionId);
|
|
494
821
|
api.close();
|
|
495
822
|
params?.__closeWorkbenchOnDone?.();
|
|
496
823
|
} catch (err) {
|
|
@@ -515,7 +842,7 @@ function QuestionsPane({ api, params, className }) {
|
|
|
515
842
|
/* @__PURE__ */ jsx2(Button, { asChild: true, size: "sm", children: /* @__PURE__ */ jsx2(QuestionSubmitButton, { children: question.schema.submitLabel ?? "Send answers" }) })
|
|
516
843
|
] })
|
|
517
844
|
] })
|
|
518
|
-
] }) }) : null,
|
|
845
|
+
] }) }, question.questionId) : null,
|
|
519
846
|
question && question.status !== "ready" ? /* @__PURE__ */ jsx2(PaneBody, { className: "p-5", children: /* @__PURE__ */ jsx2(Notice, { children: /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-2", children: [
|
|
520
847
|
/* @__PURE__ */ jsx2(XCircle, { className: "h-4 w-4 text-muted-foreground" }),
|
|
521
848
|
"Question ",
|
|
@@ -551,12 +878,12 @@ var askUserPlugin = definePlugin({
|
|
|
551
878
|
// No inner kind guard — the workspace's surface registry already
|
|
552
879
|
// pre-filters by the top-level `kind` field before calling resolve.
|
|
553
880
|
resolve(request) {
|
|
554
|
-
const
|
|
881
|
+
const sessionId = typeof request.meta === "object" && request.meta && typeof request.meta.sessionId === "string" ? request.meta.sessionId : void 0;
|
|
555
882
|
return {
|
|
556
883
|
component: ASK_USER_PANEL_ID,
|
|
557
884
|
id: ASK_USER_PANEL_ID,
|
|
558
885
|
title: ASK_USER_PANEL_TITLE,
|
|
559
|
-
params: { questionId: request.target,
|
|
886
|
+
params: { questionId: request.target, sessionId }
|
|
560
887
|
};
|
|
561
888
|
}
|
|
562
889
|
}
|